Make the config module a singleton

Change-type: patch
Co-authored-by: Pagan Gazzard <page@balena.io>
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2020-06-02 14:29:05 +01:00
parent 10a5fb6aaf
commit ff4a31a0e6
29 changed files with 492 additions and 550 deletions

View File

@ -9,7 +9,7 @@ import { PinejsClientRequest, StatusError } from 'pinejs-client-request';
import * as url from 'url'; import * as url from 'url';
import * as deviceRegister from './lib/register-device'; import * as deviceRegister from './lib/register-device';
import Config, { ConfigType } from './config'; import * as config from './config';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import { loadBackupFromMigration } from './lib/migration'; import { loadBackupFromMigration } from './lib/migration';
@ -41,7 +41,6 @@ const INTERNAL_STATE_KEYS = [
]; ];
export interface APIBinderConstructOpts { export interface APIBinderConstructOpts {
config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
logger: Logger; logger: Logger;
} }
@ -63,12 +62,11 @@ interface DeviceTag {
value: string; value: string;
} }
type KeyExchangeOpts = ConfigType<'provisioningOptions'>; type KeyExchangeOpts = config.ConfigType<'provisioningOptions'>;
export class APIBinder { export class APIBinder {
public router: express.Router; public router: express.Router;
private config: Config;
private deviceState: DeviceState; private deviceState: DeviceState;
private eventTracker: EventTracker; private eventTracker: EventTracker;
private logger: Logger; private logger: Logger;
@ -91,8 +89,7 @@ export class APIBinder {
private targetStateFetchErrors = 0; private targetStateFetchErrors = 0;
private readyForUpdates = false; private readyForUpdates = false;
public constructor({ config, eventTracker, logger }: APIBinderConstructOpts) { public constructor({ eventTracker, logger }: APIBinderConstructOpts) {
this.config = config;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.logger = logger; this.logger = logger;
@ -108,7 +105,7 @@ export class APIBinder {
appUpdatePollInterval, appUpdatePollInterval,
unmanaged, unmanaged,
connectivityCheckEnabled, connectivityCheckEnabled,
} = await this.config.getMany([ } = await config.getMany([
'appUpdatePollInterval', 'appUpdatePollInterval',
'unmanaged', 'unmanaged',
'connectivityCheckEnabled', 'connectivityCheckEnabled',
@ -160,11 +157,7 @@ export class APIBinder {
} }
public async initClient() { public async initClient() {
const { const { unmanaged, apiEndpoint, currentApiKey } = await config.getMany([
unmanaged,
apiEndpoint,
currentApiKey,
} = await this.config.getMany([
'unmanaged', 'unmanaged',
'apiEndpoint', 'apiEndpoint',
'currentApiKey', 'currentApiKey',
@ -188,7 +181,7 @@ export class APIBinder {
} }
public async start() { public async start() {
const conf = await this.config.getMany([ const conf = await config.getMany([
'apiEndpoint', 'apiEndpoint',
'unmanaged', 'unmanaged',
'bootstrapRetryDelay', 'bootstrapRetryDelay',
@ -203,14 +196,14 @@ export class APIBinder {
// value to '', to ensure that when we do re-provision, we'll report // value to '', to ensure that when we do re-provision, we'll report
// the config and hardward-specific options won't be lost // the config and hardward-specific options won't be lost
if (!apiEndpoint) { if (!apiEndpoint) {
await this.config.set({ initialConfigReported: '' }); await config.set({ initialConfigReported: '' });
} }
return; return;
} }
log.debug('Ensuring device is provisioned'); log.debug('Ensuring device is provisioned');
await this.provisionDevice(); await this.provisionDevice();
const conf2 = await this.config.getMany([ const conf2 = await config.getMany([
'initialConfigReported', 'initialConfigReported',
'apiEndpoint', 'apiEndpoint',
]); ]);
@ -278,7 +271,7 @@ export class APIBinder {
} }
public async patchDevice(id: number, updatedFields: Dictionary<unknown>) { public async patchDevice(id: number, updatedFields: Dictionary<unknown>) {
const conf = await this.config.getMany([ const conf = await config.getMany([
'unmanaged', 'unmanaged',
'provisioned', 'provisioned',
'apiTimeout', 'apiTimeout',
@ -308,7 +301,7 @@ export class APIBinder {
} }
public async provisionDependentDevice(device: Device): Promise<Device> { public async provisionDependentDevice(device: Device): Promise<Device> {
const conf = await this.config.getMany([ const conf = await config.getMany([
'unmanaged', 'unmanaged',
'provisioned', 'provisioned',
'apiTimeout', 'apiTimeout',
@ -341,7 +334,7 @@ export class APIBinder {
} }
public async getTargetState(): Promise<TargetState> { public async getTargetState(): Promise<TargetState> {
const { uuid, apiEndpoint, apiTimeout } = await this.config.getMany([ const { uuid, apiEndpoint, apiTimeout } = await config.getMany([
'uuid', 'uuid',
'apiEndpoint', 'apiEndpoint',
'apiTimeout', 'apiTimeout',
@ -377,7 +370,7 @@ export class APIBinder {
'Trying to start poll without initializing API client', 'Trying to start poll without initializing API client',
); );
} }
this.config config
.get('instantUpdates') .get('instantUpdates')
.catch(() => { .catch(() => {
// Default to skipping the initial update if we couldn't fetch the setting // Default to skipping the initial update if we couldn't fetch the setting
@ -414,7 +407,7 @@ export class APIBinder {
); );
} }
const deviceId = await this.config.get('deviceId'); const deviceId = await config.get('deviceId');
if (deviceId == null) { if (deviceId == null) {
throw new Error('Attempt to retrieve device tags before provision'); throw new Error('Attempt to retrieve device tags before provision');
} }
@ -523,7 +516,7 @@ export class APIBinder {
} }
private report = _.throttle(async () => { private report = _.throttle(async () => {
const conf = await this.config.getMany([ const conf = await config.getMany([
'deviceId', 'deviceId',
'apiTimeout', 'apiTimeout',
'apiEndpoint', 'apiEndpoint',
@ -586,7 +579,7 @@ export class APIBinder {
this.eventTracker.track('Device state report failure', { error: e }); this.eventTracker.track('Device state report failure', { error: e });
// We use the poll interval as the upper limit of // We use the poll interval as the upper limit of
// the exponential backoff // the exponential backoff
const maxDelay = await this.config.get('appUpdatePollInterval'); const maxDelay = await config.get('appUpdatePollInterval');
const delay = Math.min( const delay = Math.min(
2 ** this.stateReportErrors * MINIMUM_BACKOFF_DELAY, 2 ** this.stateReportErrors * MINIMUM_BACKOFF_DELAY,
maxDelay, maxDelay,
@ -637,7 +630,7 @@ export class APIBinder {
private async pollTargetState(skipFirstGet: boolean = false): Promise<void> { private async pollTargetState(skipFirstGet: boolean = false): Promise<void> {
let appUpdatePollInterval; let appUpdatePollInterval;
try { try {
appUpdatePollInterval = await this.config.get('appUpdatePollInterval'); appUpdatePollInterval = await config.get('appUpdatePollInterval');
if (!skipFirstGet) { if (!skipFirstGet) {
await this.getAndSetTargetState(false); await this.getAndSetTargetState(false);
this.targetStateFetchErrors = 0; this.targetStateFetchErrors = 0;
@ -673,7 +666,7 @@ export class APIBinder {
} }
try { try {
const deviceId = await this.config.get('deviceId'); const deviceId = await config.get('deviceId');
if (deviceId == null) { if (deviceId == null) {
throw new InternalInconsistencyError( throw new InternalInconsistencyError(
@ -710,7 +703,7 @@ export class APIBinder {
// Set the config value for pinDevice to null, so that we know the // Set the config value for pinDevice to null, so that we know the
// task has been completed // task has been completed
await this.config.remove('pinDevice'); await config.remove('pinDevice');
} catch (e) { } catch (e) {
log.error(`Could not pin device to release! ${e}`); log.error(`Could not pin device to release! ${e}`);
throw e; throw e;
@ -742,7 +735,7 @@ export class APIBinder {
const targetConfig = await this.deviceState.deviceConfig.formatConfigKeys( const targetConfig = await this.deviceState.deviceConfig.formatConfigKeys(
targetConfigUnformatted, targetConfigUnformatted,
); );
const deviceId = await this.config.get('deviceId'); const deviceId = await config.get('deviceId');
if (!currentState.local.config) { if (!currentState.local.config) {
throw new InternalInconsistencyError( throw new InternalInconsistencyError(
@ -774,7 +767,7 @@ export class APIBinder {
} }
} }
await this.config.set({ initialConfigReported: apiEndpoint }); await config.set({ initialConfigReported: apiEndpoint });
} }
private async reportInitialConfig( private async reportInitialConfig(
@ -794,7 +787,7 @@ export class APIBinder {
opts?: KeyExchangeOpts, opts?: KeyExchangeOpts,
): Promise<Device> { ): Promise<Device> {
if (opts == null) { if (opts == null) {
opts = await this.config.get('provisioningOptions'); opts = await config.get('provisioningOptions');
} }
const uuid = opts.uuid; const uuid = opts.uuid;
@ -867,7 +860,7 @@ export class APIBinder {
} catch (e) { } catch (e) {
if (e instanceof ExchangeKeyError) { if (e instanceof ExchangeKeyError) {
log.error('Exchanging key failed, re-registering...'); log.error('Exchanging key failed, re-registering...');
await this.config.regenerateRegistrationFields(); await config.regenerateRegistrationFields();
} }
throw e; throw e;
} }
@ -875,7 +868,7 @@ export class APIBinder {
private async provision() { private async provision() {
let device: Device | null = null; let device: Device | null = null;
const opts = await this.config.get('provisioningOptions'); const opts = await config.get('provisioningOptions');
if ( if (
opts.registered_at == null || opts.registered_at == null ||
opts.deviceId == null || opts.deviceId == null ||
@ -926,12 +919,12 @@ export class APIBinder {
deviceId: id, deviceId: id,
apiKey: null, apiKey: null,
}; };
await this.config.set(configToUpdate); await config.set(configToUpdate);
this.eventTracker.track('Device bootstrap success'); this.eventTracker.track('Device bootstrap success');
} }
// Now check if we need to pin the device // Now check if we need to pin the device
const pinValue = await this.config.get('pinDevice'); const pinValue = await config.get('pinDevice');
if (pinValue != null) { if (pinValue != null) {
if (pinValue.app == null || pinValue.commit == null) { if (pinValue.app == null || pinValue.commit == null) {
@ -965,7 +958,7 @@ export class APIBinder {
'Trying to provision a device without initializing API client', 'Trying to provision a device without initializing API client',
); );
} }
const conf = await this.config.getMany([ const conf = await config.getMany([
'provisioned', 'provisioned',
'bootstrapRetryDelay', 'bootstrapRetryDelay',
'apiKey', 'apiKey',
@ -996,7 +989,7 @@ export class APIBinder {
router.post('/v1/update', (req, res, next) => { router.post('/v1/update', (req, res, next) => {
apiBinder.eventTracker.track('Update notification'); apiBinder.eventTracker.track('Update notification');
if (apiBinder.readyForUpdates) { if (apiBinder.readyForUpdates) {
this.config config
.get('instantUpdates') .get('instantUpdates')
.then((instantUpdates) => { .then((instantUpdates) => {
if (instantUpdates) { if (instantUpdates) {

View File

@ -13,7 +13,7 @@ import ServiceManager from './compose/service-manager';
import DeviceState from './device-state'; import DeviceState from './device-state';
import { APIBinder } from './api-binder'; import { APIBinder } from './api-binder';
import Config from './config'; import * as config from './config';
import NetworkManager from './compose/network-manager'; import NetworkManager from './compose/network-manager';
import VolumeManager from './compose/volume-manager'; import VolumeManager from './compose/volume-manager';
@ -57,7 +57,6 @@ class ApplicationManager extends EventEmitter {
public services: ServiceManager; public services: ServiceManager;
public volumes: VolumeManager; public volumes: VolumeManager;
public networks: NetworkManager; public networks: NetworkManager;
public config: Config;
public images: ImageManager; public images: ImageManager;
public proxyvisor: any; public proxyvisor: any;
@ -70,7 +69,6 @@ class ApplicationManager extends EventEmitter {
public constructor({ public constructor({
logger: Logger, logger: Logger,
config: Config,
eventTracker: EventTracker, eventTracker: EventTracker,
deviceState: DeviceState, deviceState: DeviceState,
apiBinder: APIBinder, apiBinder: APIBinder,

View File

@ -8,6 +8,7 @@ import * as path from 'path';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import { log } from './lib/supervisor-console'; import { log } from './lib/supervisor-console';
import * as config from './config';
import { validateTargetContracts } from './lib/contracts'; import { validateTargetContracts } from './lib/contracts';
import { DockerUtils as Docker } from './lib/docker-utils'; import { DockerUtils as Docker } from './lib/docker-utils';
@ -77,7 +78,7 @@ const createApplicationManagerRouter = function (applications) {
}; };
export class ApplicationManager extends EventEmitter { export class ApplicationManager extends EventEmitter {
constructor({ logger, config, eventTracker, deviceState, apiBinder }) { constructor({ logger, eventTracker, deviceState, apiBinder }) {
super(); super();
this.serviceAction = serviceAction; this.serviceAction = serviceAction;
@ -168,7 +169,6 @@ export class ApplicationManager extends EventEmitter {
this.localModeSwitchCompletion = this.localModeSwitchCompletion.bind(this); this.localModeSwitchCompletion = this.localModeSwitchCompletion.bind(this);
this.reportOptionalContainers = this.reportOptionalContainers.bind(this); this.reportOptionalContainers = this.reportOptionalContainers.bind(this);
this.logger = logger; this.logger = logger;
this.config = config;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.deviceState = deviceState; this.deviceState = deviceState;
this.apiBinder = apiBinder; this.apiBinder = apiBinder;
@ -176,12 +176,10 @@ export class ApplicationManager extends EventEmitter {
this.images = new Images({ this.images = new Images({
docker: this.docker, docker: this.docker,
logger: this.logger, logger: this.logger,
config: this.config,
}); });
this.services = new ServiceManager({ this.services = new ServiceManager({
docker: this.docker, docker: this.docker,
logger: this.logger, logger: this.logger,
config: this.config,
}); });
this.networks = new NetworkManager({ this.networks = new NetworkManager({
docker: this.docker, docker: this.docker,
@ -192,25 +190,20 @@ export class ApplicationManager extends EventEmitter {
logger: this.logger, logger: this.logger,
}); });
this.proxyvisor = new Proxyvisor({ this.proxyvisor = new Proxyvisor({
config: this.config,
logger: this.logger, logger: this.logger,
docker: this.docker, docker: this.docker,
images: this.images, images: this.images,
applications: this, applications: this,
}); });
this.localModeManager = new LocalModeManager( this.localModeManager = new LocalModeManager(this.docker, this.logger);
this.config,
this.docker,
this.logger,
);
this.timeSpentFetching = 0; this.timeSpentFetching = 0;
this.fetchesInProgress = 0; this.fetchesInProgress = 0;
this._targetVolatilePerImageId = {}; this._targetVolatilePerImageId = {};
this._containerStarted = {}; this._containerStarted = {};
this.targetStateWrapper = new TargetStateAccessor(this, this.config); this.targetStateWrapper = new TargetStateAccessor(this);
this.config.on('change', (changedConfig) => { config.on('change', (changedConfig) => {
if (changedConfig.appUpdatePollInterval) { if (changedConfig.appUpdatePollInterval) {
this.images.appUpdatePollInterval = changedConfig.appUpdatePollInterval; this.images.appUpdatePollInterval = changedConfig.appUpdatePollInterval;
} }
@ -223,7 +216,6 @@ export class ApplicationManager extends EventEmitter {
volumes: this.volumes, volumes: this.volumes,
applications: this, applications: this,
images: this.images, images: this.images,
config: this.config,
callbacks: { callbacks: {
containerStarted: (id) => { containerStarted: (id) => {
this._containerStarted[id] = true; this._containerStarted[id] = true;
@ -257,7 +249,7 @@ export class ApplicationManager extends EventEmitter {
} }
init() { init() {
return this.config return config
.get('appUpdatePollInterval') .get('appUpdatePollInterval')
.then((interval) => { .then((interval) => {
this.images.appUpdatePollInterval = interval; this.images.appUpdatePollInterval = interval;
@ -297,7 +289,7 @@ export class ApplicationManager extends EventEmitter {
return Promise.join( return Promise.join(
this.services.getStatus(), this.services.getStatus(),
this.images.getStatus(), this.images.getStatus(),
this.config.get('currentCommit'), config.get('currentCommit'),
function (services, images, currentCommit) { function (services, images, currentCommit) {
const apps = {}; const apps = {};
const dependent = {}; const dependent = {};
@ -430,7 +422,7 @@ export class ApplicationManager extends EventEmitter {
this.services.getAll(), this.services.getAll(),
this.networks.getAll(), this.networks.getAll(),
this.volumes.getAll(), this.volumes.getAll(),
this.config.get('currentCommit'), config.get('currentCommit'),
this._buildApps, this._buildApps,
); );
} }
@ -440,7 +432,7 @@ export class ApplicationManager extends EventEmitter {
this.services.getAllByAppId(appId), this.services.getAllByAppId(appId),
this.networks.getAllByAppId(appId), this.networks.getAllByAppId(appId),
this.volumes.getAllByAppId(appId), this.volumes.getAllByAppId(appId),
this.config.get('currentCommit'), config.get('currentCommit'),
this._buildApps, this._buildApps,
).get(appId); ).get(appId);
} }
@ -1083,7 +1075,7 @@ export class ApplicationManager extends EventEmitter {
normaliseAndExtendAppFromDB(app) { normaliseAndExtendAppFromDB(app) {
return Promise.join( return Promise.join(
this.config.get('extendedEnvOptions'), config.get('extendedEnvOptions'),
this.docker this.docker
.getNetworkGateway(constants.supervisorNetworkInterface) .getNetworkGateway(constants.supervisorNetworkInterface)
.catch(() => '127.0.0.1'), .catch(() => '127.0.0.1'),
@ -1584,7 +1576,7 @@ export class ApplicationManager extends EventEmitter {
if (skipLock) { if (skipLock) {
return Promise.try(fn); return Promise.try(fn);
} }
return this.config return config
.get('lockOverride') .get('lockOverride')
.then((lockOverride) => lockOverride || force) .then((lockOverride) => lockOverride || force)
.then((lockOverridden) => .then((lockOverridden) =>
@ -1618,13 +1610,13 @@ export class ApplicationManager extends EventEmitter {
containerIdsByAppId[intId] = this.services.getContainerIdMap(intId); containerIdsByAppId[intId] = this.services.getContainerIdMap(intId);
}); });
return this.config.get('localMode').then((localMode) => { return config.get('localMode').then((localMode) => {
return Promise.props({ return Promise.props({
cleanupNeeded: this.images.isCleanupNeeded(), cleanupNeeded: this.images.isCleanupNeeded(),
availableImages: this.images.getAvailable(), availableImages: this.images.getAvailable(),
downloading: this.images.getDownloadingImageIds(), downloading: this.images.getDownloadingImageIds(),
supervisorNetworkReady: this.networks.supervisorNetworkReady(), supervisorNetworkReady: this.networks.supervisorNetworkReady(),
delta: this.config.get('delta'), delta: config.get('delta'),
containerIds: Promise.props(containerIdsByAppId), containerIds: Promise.props(containerIdsByAppId),
localMode, localMode,
}); });

View File

@ -1,6 +1,6 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import Config from '../config'; import * as config from '../config';
import { ApplicationManager } from '../application-manager'; import { ApplicationManager } from '../application-manager';
import Images, { Image } from './images'; import Images, { Image } from './images';
@ -140,7 +140,6 @@ export function getExecutors(app: {
volumes: VolumeManager; volumes: VolumeManager;
applications: ApplicationManager; applications: ApplicationManager;
images: Images; images: Images;
config: Config;
callbacks: CompositionCallbacks; callbacks: CompositionCallbacks;
}) { }) {
const executors: Executors<CompositionStepAction> = { const executors: Executors<CompositionStepAction> = {
@ -223,7 +222,7 @@ export function getExecutors(app: {
app.callbacks.containerStarted(container.id); app.callbacks.containerStarted(container.id);
}, },
updateCommit: async (step) => { updateCommit: async (step) => {
await app.config.set({ currentCommit: step.target }); await config.set({ currentCommit: step.target });
}, },
handover: (step) => { handover: (step) => {
return app.lockFn( return app.lockFn(
@ -241,7 +240,7 @@ export function getExecutors(app: {
const startTime = process.hrtime(); const startTime = process.hrtime();
app.callbacks.fetchStart(); app.callbacks.fetchStart();
const [fetchOpts, availableImages] = await Promise.all([ const [fetchOpts, availableImages] = await Promise.all([
app.config.get('fetchOptions'), config.get('fetchOptions'),
app.images.getAvailable(), app.images.getAvailable(),
]); ]);
@ -276,7 +275,7 @@ export function getExecutors(app: {
await app.images.save(step.image); await app.images.save(step.image);
}, },
cleanup: async () => { cleanup: async () => {
const localMode = await app.config.get('localMode'); const localMode = await config.get('localMode');
if (!localMode) { if (!localMode) {
await app.images.cleanup(); await app.images.cleanup();
} }

View File

@ -4,7 +4,6 @@ import { EventEmitter } from 'events';
import * as _ from 'lodash'; import * as _ from 'lodash';
import StrictEventEmitter from 'strict-event-emitter-types'; import StrictEventEmitter from 'strict-event-emitter-types';
import Config from '../config';
import * as db from '../db'; import * as db from '../db';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { import {
@ -29,7 +28,6 @@ type ImageEventEmitter = StrictEventEmitter<EventEmitter, ImageEvents>;
interface ImageConstructOpts { interface ImageConstructOpts {
docker: DockerUtils; docker: DockerUtils;
logger: Logger; logger: Logger;
config: Config;
} }
interface FetchProgressEvent { interface FetchProgressEvent {

View File

@ -7,7 +7,7 @@ import * as _ from 'lodash';
import { fs } from 'mz'; import { fs } from 'mz';
import StrictEventEmitter from 'strict-event-emitter-types'; import StrictEventEmitter from 'strict-event-emitter-types';
import Config from '../config'; import * as config from '../config';
import Docker from '../lib/docker-utils'; import Docker from '../lib/docker-utils';
import Logger from '../logger'; import Logger from '../logger';
@ -28,7 +28,6 @@ import log from '../lib/supervisor-console';
interface ServiceConstructOpts { interface ServiceConstructOpts {
docker: Docker; docker: Docker;
logger: Logger; logger: Logger;
config: Config;
} }
interface ServiceManagerEvents { interface ServiceManagerEvents {
@ -47,7 +46,6 @@ interface KillOpts {
export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) { export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) {
private docker: Docker; private docker: Docker;
private logger: Logger; private logger: Logger;
private config: Config;
// Whether a container has died, indexed by ID // Whether a container has died, indexed by ID
private containerHasDied: Dictionary<boolean> = {}; private containerHasDied: Dictionary<boolean> = {};
@ -60,7 +58,6 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
super(); super();
this.docker = opts.docker; this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
this.config = opts.config;
} }
public async getAll( public async getAll(
@ -239,7 +236,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
} }
public async create(service: Service) { public async create(service: Service) {
const mockContainerId = this.config.newUniqueKey(); const mockContainerId = config.newUniqueKey();
try { try {
const existing = await this.get(service); const existing = await this.get(service);
if (existing.containerId == null) { if (existing.containerId == null) {
@ -257,7 +254,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
throw e; throw e;
} }
const deviceName = await this.config.get('name'); const deviceName = await config.get('name');
if (!isValidDeviceName(deviceName)) { if (!isValidDeviceName(deviceName)) {
throw new Error( throw new Error(
'The device name contains a newline, which is unsupported by balena. ' + 'The device name contains a newline, which is unsupported by balena. ' +
@ -340,7 +337,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
message.trim().match(/exec format error$/) message.trim().match(/exec format error$/)
) { ) {
// Provide a friendlier error message for "exec format error" // Provide a friendlier error message for "exec format error"
const deviceType = await this.config.get('deviceType'); const deviceType = await config.get('deviceType');
err = new Error( err = new Error(
`Application architecture incompatible with ${deviceType}: exec format error`, `Application architecture incompatible with ${deviceType}: exec format error`,
); );

View File

@ -5,7 +5,7 @@ import { URL } from 'url';
import supervisorVersion = require('../lib/supervisor-version'); import supervisorVersion = require('../lib/supervisor-version');
import Config from '.'; import * as config from '.';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import * as osRelease from '../lib/os-release'; import * as osRelease from '../lib/os-release';
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
@ -14,14 +14,14 @@ export const fnSchema = {
version: () => { version: () => {
return Bluebird.resolve(supervisorVersion); return Bluebird.resolve(supervisorVersion);
}, },
currentApiKey: (config: Config) => { currentApiKey: () => {
return config return config
.getMany(['apiKey', 'deviceApiKey']) .getMany(['apiKey', 'deviceApiKey'])
.then(({ apiKey, deviceApiKey }) => { .then(({ apiKey, deviceApiKey }) => {
return apiKey || deviceApiKey; return apiKey || deviceApiKey;
}); });
}, },
provisioned: (config: Config) => { provisioned: () => {
return config return config
.getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId']) .getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId'])
.then((requiredValues) => { .then((requiredValues) => {
@ -50,7 +50,7 @@ export const fnSchema = {
return 'unknown'; return 'unknown';
} }
}, },
provisioningOptions: (config: Config) => { provisioningOptions: () => {
return config return config
.getMany([ .getMany([
'uuid', 'uuid',
@ -79,7 +79,7 @@ export const fnSchema = {
}; };
}); });
}, },
mixpanelHost: (config: Config) => { mixpanelHost: () => {
return config.get('apiEndpoint').then((apiEndpoint) => { return config.get('apiEndpoint').then((apiEndpoint) => {
if (!apiEndpoint) { if (!apiEndpoint) {
return null; return null;
@ -88,7 +88,7 @@ export const fnSchema = {
return { host: url.host, path: '/mixpanel' }; return { host: url.host, path: '/mixpanel' };
}); });
}, },
extendedEnvOptions: (config: Config) => { extendedEnvOptions: () => {
return config.getMany([ return config.getMany([
'uuid', 'uuid',
'listenPort', 'listenPort',
@ -102,7 +102,7 @@ export const fnSchema = {
'osVersion', 'osVersion',
]); ]);
}, },
fetchOptions: (config: Config) => { fetchOptions: () => {
return config.getMany([ return config.getMany([
'uuid', 'uuid',
'currentApiKey', 'currentApiKey',
@ -116,7 +116,7 @@ export const fnSchema = {
'deltaVersion', 'deltaVersion',
]); ]);
}, },
unmanaged: (config: Config) => { unmanaged: () => {
return config.get('apiEndpoint').then((apiEndpoint) => { return config.get('apiEndpoint').then((apiEndpoint) => {
return !apiEndpoint; return !apiEndpoint;
}); });

View File

@ -1,4 +1,3 @@
import * as Bluebird from 'bluebird';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Transaction } from 'knex'; import { Transaction } from 'knex';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -21,10 +20,6 @@ import {
InternalInconsistencyError, InternalInconsistencyError,
} from '../lib/errors'; } from '../lib/errors';
interface ConfigOpts {
configPath?: string;
}
export type ConfigMap<T extends SchemaTypeKey> = { export type ConfigMap<T extends SchemaTypeKey> = {
[key in T]: SchemaReturn<key>; [key in T]: SchemaReturn<key>;
}; };
@ -36,181 +31,235 @@ export type ConfigChangeMap<T extends SchemaTypeKey> = {
export type ConfigKey = SchemaTypeKey; export type ConfigKey = SchemaTypeKey;
export type ConfigType<T extends ConfigKey> = SchemaReturn<T>; export type ConfigType<T extends ConfigKey> = SchemaReturn<T>;
interface ConfigEvents { interface ConfigEventTypes {
change: ConfigChangeMap<SchemaTypeKey>; change: ConfigChangeMap<SchemaTypeKey>;
} }
type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEvents>; export const configJsonBackend: ConfigJsonConfigBackend = new ConfigJsonConfigBackend(
Schema.schema,
);
export class Config extends (EventEmitter as new () => ConfigEventEmitter) { type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEventTypes>;
private configJsonBackend: ConfigJsonConfigBackend; class ConfigEvents extends (EventEmitter as new () => ConfigEventEmitter) {}
const events = new ConfigEvents();
public constructor({ configPath }: ConfigOpts = {}) { // Expose methods which make this module act as an EventEmitter
super(); export const on: typeof events['on'] = events.on.bind(events);
this.configJsonBackend = new ConfigJsonConfigBackend( export const once: typeof events['once'] = events.once.bind(events);
Schema.schema, export const removeListener: typeof events['removeListener'] = events.removeListener.bind(
configPath, events,
); );
export const removeAllListeners: typeof events['removeAllListeners'] = events.removeAllListeners.bind(
events,
);
export async function get<T extends SchemaTypeKey>(
key: T,
trx?: Transaction,
): Promise<SchemaReturn<T>> {
const $db = trx || db.models.bind(db);
if (Schema.schema.hasOwnProperty(key)) {
const schemaKey = key as Schema.SchemaKey;
return getSchema(schemaKey, $db).then((value) => {
if (value == null) {
const defaultValue = schemaTypes[key].default;
if (defaultValue instanceof t.Type) {
// The only reason that this would be the case in a non-function
// schema key is for the meta nullOrUndefined value. We check this
// by first decoding the value undefined with the default type, and
// then return undefined
const maybeDecoded = (defaultValue as t.Type<any>).decode(undefined);
return (
checkValueDecode(maybeDecoded, key, undefined) && maybeDecoded.right
);
}
return defaultValue as SchemaReturn<T>;
}
const decoded = decodeSchema(schemaKey, value);
// The following function will throw if the value
// is not correct, so we chain it this way to keep
// the type system happy
return checkValueDecode(decoded, key, value) && decoded.right;
});
} else if (FnSchema.fnSchema.hasOwnProperty(key)) {
const fnKey = key as FnSchema.FnSchemaKey;
// Cast the promise as something that produces an unknown, and this means that
// we can validate the output of the function as well, ensuring that the type matches
const promiseValue = FnSchema.fnSchema[fnKey]();
return promiseValue.then((value: unknown) => {
const decoded = schemaTypes[key].type.decode(value);
return checkValueDecode(decoded, key, value) && decoded.right;
});
} else {
throw new Error(`Unknown config value ${key}`);
} }
}
public async init() { export async function getMany<T extends SchemaTypeKey>(
await this.generateRequiredFields(); keys: T[],
} trx?: Transaction,
): Promise<{ [key in T]: SchemaReturn<key> }> {
const values = await Promise.all(keys.map((k) => get(k, trx)));
return (_.zipObject(keys, values) as unknown) as Promise<
{ [key in T]: SchemaReturn<key> }
>;
}
public get<T extends SchemaTypeKey>( export async function set<T extends SchemaTypeKey>(
key: T, keyValues: ConfigMap<T>,
trx?: Transaction, trx?: Transaction,
): Bluebird<SchemaReturn<T>> { ): Promise<void> {
const $db = trx || db.models.bind(db); const setValuesInTransaction = async (tx: Transaction) => {
const configJsonVals: Dictionary<unknown> = {};
const dbVals: Dictionary<unknown> = {};
return Bluebird.try(() => { _.each(keyValues, (v, k: T) => {
if (Schema.schema.hasOwnProperty(key)) { const schemaKey = k as Schema.SchemaKey;
const schemaKey = key as Schema.SchemaKey; const source = Schema.schema[schemaKey].source;
return this.getSchema(schemaKey, $db).then((value) => { switch (source) {
if (value == null) { case 'config.json':
const defaultValue = schemaTypes[key].default; configJsonVals[schemaKey] = v;
if (defaultValue instanceof t.Type) { break;
// The only reason that this would be the case in a non-function case 'db':
// schema key is for the meta nullOrUndefined value. We check this dbVals[schemaKey] = v;
// by first decoding the value undefined with the default type, and break;
// then return undefined default:
const maybeDecoded = (defaultValue as t.Type<any>).decode( throw new Error(
undefined, `Unknown configuration source: ${source} for config key: ${k}`,
); );
return (
this.checkValueDecode(maybeDecoded, key, undefined) &&
maybeDecoded.right
);
}
return defaultValue as SchemaReturn<T>;
}
const decoded = this.decodeSchema(schemaKey, value);
// The following function will throw if the value
// is not correct, so we chain it this way to keep
// the type system happy
return this.checkValueDecode(decoded, key, value) && decoded.right;
});
} else if (FnSchema.fnSchema.hasOwnProperty(key)) {
const fnKey = key as FnSchema.FnSchemaKey;
// Cast the promise as something that produces an unknown, and this means that
// we can validate the output of the function as well, ensuring that the type matches
const promiseValue = FnSchema.fnSchema[fnKey](this) as Bluebird<
unknown
>;
return promiseValue.then((value: unknown) => {
const decoded = schemaTypes[key].type.decode(value);
return this.checkValueDecode(decoded, key, value) && decoded.right;
});
} else {
throw new Error(`Unknown config value ${key}`);
} }
}); });
}
public getMany<T extends SchemaTypeKey>( const dbKeys = _.keys(dbVals) as T[];
keys: T[], const oldValues = await getMany(dbKeys, tx);
trx?: Transaction, await Promise.all(
): Bluebird<{ [key in T]: SchemaReturn<key> }> { dbKeys.map(async (key: T) => {
return Bluebird.map(keys, (key: T) => this.get(key, trx)).then((values) => {
return _.zipObject(keys, values);
}) as Bluebird<{ [key in T]: SchemaReturn<key> }>;
}
public async set<T extends SchemaTypeKey>(
keyValues: ConfigMap<T>,
trx?: Transaction,
): Promise<void> {
const setValuesInTransaction = async (tx: Transaction) => {
const configJsonVals: Dictionary<unknown> = {};
const dbVals: Dictionary<unknown> = {};
_.each(keyValues, (v, k: T) => {
const schemaKey = k as Schema.SchemaKey;
const source = Schema.schema[schemaKey].source;
switch (source) {
case 'config.json':
configJsonVals[schemaKey] = v;
break;
case 'db':
dbVals[schemaKey] = v;
break;
default:
throw new Error(
`Unknown configuration source: ${source} for config key: ${k}`,
);
}
});
const dbKeys = _.keys(dbVals) as T[];
const oldValues = await this.getMany(dbKeys, tx);
await Bluebird.map(dbKeys, async (key: T) => {
const value = dbVals[key]; const value = dbVals[key];
// if we have anything other than a string, it must be converted to // if we have anything other than a string, it must be converted to
// a string before being stored in the db // a string before being stored in the db
const strValue = Config.valueToString(value, key); const strValue = valueToString(value, key);
if (oldValues[key] !== value) { if (oldValues[key] !== value) {
await db.upsertModel('config', { key, value: strValue }, { key }, tx); await db.upsertModel('config', { key, value: strValue }, { key }, tx);
} }
}); }),
);
if (!_.isEmpty(configJsonVals)) { if (!_.isEmpty(configJsonVals)) {
await this.configJsonBackend.set( await configJsonBackend.set(
configJsonVals as { configJsonVals as {
[name in Schema.SchemaKey]: unknown; [name in Schema.SchemaKey]: unknown;
}, },
);
}
};
// Firstly validate and coerce all of the types as
// they are being set
keyValues = this.validateConfigMap(keyValues);
if (trx != null) {
await setValuesInTransaction(trx);
} else {
await db.transaction((tx: Transaction) => setValuesInTransaction(tx));
}
this.emit('change', keyValues as ConfigMap<SchemaTypeKey>);
}
public async remove<T extends Schema.SchemaKey>(key: T): Promise<void> {
if (Schema.schema[key] == null || !Schema.schema[key].mutable) {
throw new Error(`Attempt to delete non-existent or immutable key ${key}`);
}
if (Schema.schema[key].source === 'config.json') {
return this.configJsonBackend.remove(key);
} else if (Schema.schema[key].source === 'db') {
await db.models('config').del().where({ key });
} else {
throw new Error(
`Unknown or unsupported config backend: ${Schema.schema[key].source}`,
); );
} }
};
// Firstly validate and coerce all of the types as
// they are being set
keyValues = validateConfigMap(keyValues);
if (trx != null) {
await setValuesInTransaction(trx);
} else {
await db.transaction((tx: Transaction) => setValuesInTransaction(tx));
}
events.emit('change', keyValues as ConfigMap<SchemaTypeKey>);
}
export async function remove<T extends Schema.SchemaKey>(
key: T,
): Promise<void> {
if (Schema.schema[key] == null || !Schema.schema[key].mutable) {
throw new Error(`Attempt to delete non-existent or immutable key ${key}`);
}
if (Schema.schema[key].source === 'config.json') {
return configJsonBackend.remove(key);
} else if (Schema.schema[key].source === 'db') {
await db.models('config').del().where({ key });
} else {
throw new Error(
`Unknown or unsupported config backend: ${Schema.schema[key].source}`,
);
}
}
export async function regenerateRegistrationFields(): Promise<void> {
await set({
uuid: newUniqueKey(),
deviceApiKey: newUniqueKey(),
});
}
export function newUniqueKey(): string {
return generateUniqueKey();
}
export function valueIsValid<T extends SchemaTypeKey>(
key: T,
value: unknown,
): boolean {
// If the default entry in the schema is a type and not a value,
// use this in the validation of the value
const schemaTypesEntry = schemaTypes[key as SchemaTypeKey];
let type: t.Type<unknown>;
if (schemaTypesEntry.default instanceof t.Type) {
type = t.union([schemaTypesEntry.type, schemaTypesEntry.default]);
} else {
type = schemaTypesEntry.type;
} }
public async regenerateRegistrationFields(): Promise<void> { return isRight(type.decode(value));
await this.set({ }
uuid: this.newUniqueKey(),
deviceApiKey: this.newUniqueKey(), async function getSchema<T extends Schema.SchemaKey>(
}); key: T,
$db: Transaction,
): Promise<unknown> {
let value: unknown;
switch (Schema.schema[key].source) {
case 'config.json':
value = await configJsonBackend.get(key);
break;
case 'db':
const [conf] = await $db('config').select('value').where({ key });
if (conf != null) {
return conf.value;
}
break;
} }
public newUniqueKey(): string { return value;
return generateUniqueKey(); }
}
function decodeSchema<T extends Schema.SchemaKey>(
key: T,
value: unknown,
): Either<t.Errors, SchemaReturn<T>> {
return schemaTypes[key].type.decode(value);
}
function validateConfigMap<T extends SchemaTypeKey>(
configMap: ConfigMap<T>,
): ConfigMap<T> {
// Just loop over every value, run the decode function, and
// throw if any value fails verification
return _.mapValues(configMap, (value, key) => {
if (
!Schema.schema.hasOwnProperty(key) ||
!Schema.schema[key as Schema.SchemaKey].mutable
) {
throw new Error(
`Attempt to set value for non-mutable schema key: ${key}`,
);
}
public valueIsValid<T extends SchemaTypeKey>(
key: T,
value: unknown,
): boolean {
// If the default entry in the schema is a type and not a value, // If the default entry in the schema is a type and not a value,
// use this in the validation of the value // use this in the validation of the value
const schemaTypesEntry = schemaTypes[key as SchemaTypeKey]; const schemaTypesEntry = schemaTypes[key as SchemaTypeKey];
@ -221,122 +270,66 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
type = schemaTypesEntry.type; type = schemaTypesEntry.type;
} }
return isRight(type.decode(value)); const decoded = type.decode(value);
} if (isLeft(decoded)) {
throw new TypeError(
private async getSchema<T extends Schema.SchemaKey>( `Cannot set value for ${key}, as value failed validation: ${inspect(
key: T, value,
$db: Transaction, { depth: Infinity },
): Promise<unknown> { )}`,
let value: unknown; );
switch (Schema.schema[key].source) {
case 'config.json':
value = await this.configJsonBackend.get(key);
break;
case 'db':
const [conf] = await $db('config').select('value').where({ key });
if (conf != null) {
return conf.value;
}
break;
} }
return decoded.right;
}) as ConfigMap<T>;
}
return value; export async function generateRequiredFields() {
} return getMany(['uuid', 'deviceApiKey', 'apiSecret', 'unmanaged']).then(
({ uuid, deviceApiKey, apiSecret, unmanaged }) => {
private decodeSchema<T extends Schema.SchemaKey>(
key: T,
value: unknown,
): Either<t.Errors, SchemaReturn<T>> {
return schemaTypes[key].type.decode(value);
}
private validateConfigMap<T extends SchemaTypeKey>(
configMap: ConfigMap<T>,
): ConfigMap<T> {
// Just loop over every value, run the decode function, and
// throw if any value fails verification
return _.mapValues(configMap, (value, key) => {
if (
!Schema.schema.hasOwnProperty(key) ||
!Schema.schema[key as Schema.SchemaKey].mutable
) {
throw new Error(
`Attempt to set value for non-mutable schema key: ${key}`,
);
}
// If the default entry in the schema is a type and not a value,
// use this in the validation of the value
const schemaTypesEntry = schemaTypes[key as SchemaTypeKey];
let type: t.Type<unknown>;
if (schemaTypesEntry.default instanceof t.Type) {
type = t.union([schemaTypesEntry.type, schemaTypesEntry.default]);
} else {
type = schemaTypesEntry.type;
}
const decoded = type.decode(value);
if (isLeft(decoded)) {
throw new TypeError(
`Cannot set value for ${key}, as value failed validation: ${inspect(
value,
{ depth: Infinity },
)}`,
);
}
return decoded.right;
}) as ConfigMap<T>;
}
private async generateRequiredFields() {
return this.getMany([
'uuid',
'deviceApiKey',
'apiSecret',
'unmanaged',
]).then(({ uuid, deviceApiKey, apiSecret, unmanaged }) => {
// These fields need to be set regardless // These fields need to be set regardless
if (uuid == null || apiSecret == null) { if (uuid == null || apiSecret == null) {
uuid = uuid || this.newUniqueKey(); uuid = uuid || newUniqueKey();
apiSecret = apiSecret || this.newUniqueKey(); apiSecret = apiSecret || newUniqueKey();
} }
return this.set({ uuid, apiSecret }).then(() => { return set({ uuid, apiSecret }).then(() => {
if (unmanaged) { if (unmanaged) {
return; return;
} }
if (!deviceApiKey) { if (!deviceApiKey) {
return this.set({ deviceApiKey: this.newUniqueKey() }); return set({ deviceApiKey: newUniqueKey() });
} }
}); });
}); },
} );
}
private static valueToString(value: unknown, name: string) { function valueToString(value: unknown, name: string) {
switch (typeof value) { switch (typeof value) {
case 'object': case 'object':
return JSON.stringify(value); return JSON.stringify(value);
case 'number': case 'number':
case 'string': case 'string':
case 'boolean': case 'boolean':
return value.toString(); return value.toString();
default: default:
throw new InternalInconsistencyError( throw new InternalInconsistencyError(
`Could not convert configuration value to string for storage, name: ${name}, value: ${value}, type: ${typeof value}`, `Could not convert configuration value to string for storage, name: ${name}, value: ${value}, type: ${typeof value}`,
); );
}
}
private checkValueDecode(
decoded: Either<t.Errors, unknown>,
key: string,
value: unknown,
): decoded is Right<unknown> {
if (isLeft(decoded)) {
throw new ConfigurationValidationError(key, value);
}
return true;
} }
} }
export default Config; function checkValueDecode(
decoded: Either<t.Errors, unknown>,
key: string,
value: unknown,
): decoded is Right<unknown> {
if (isLeft(decoded)) {
throw new ConfigurationValidationError(key, value);
}
return true;
}
export const initialized = (async () => {
await db.initialized;
await generateRequiredFields();
})();

View File

@ -5,6 +5,7 @@ import * as _ from 'lodash';
import { ApplicationManager } from '../application-manager'; import { ApplicationManager } from '../application-manager';
import { Service } from '../compose/service'; import { Service } from '../compose/service';
import Volume from '../compose/volume'; import Volume from '../compose/volume';
import * as config from '../config';
import * as db from '../db'; import * as db from '../db';
import { spawnJournalctl } from '../lib/journald'; import { spawnJournalctl } from '../lib/journald';
import { import {
@ -272,7 +273,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
router.post('/v2/local/target-state', async (req, res) => { router.post('/v2/local/target-state', async (req, res) => {
// let's first ensure that we're in local mode, otherwise // let's first ensure that we're in local mode, otherwise
// this function should not do anything // this function should not do anything
const localMode = await deviceState.config.get('localMode'); const localMode = await config.get('localMode');
if (!localMode) { if (!localMode) {
return res.status(400).json({ return res.status(400).json({
status: 'failed', status: 'failed',
@ -300,7 +301,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
router.get('/v2/local/device-info', async (_req, res) => { router.get('/v2/local/device-info', async (_req, res) => {
try { try {
const { deviceType, deviceArch } = await applications.config.getMany([ const { deviceType, deviceArch } = await config.getMany([
'deviceType', 'deviceType',
'deviceArch', 'deviceArch',
]); ]);
@ -386,7 +387,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
router.get('/v2/state/status', async (_req, res) => { router.get('/v2/state/status', async (_req, res) => {
const currentRelease = await applications.config.get('currentCommit'); const currentRelease = await config.get('currentCommit');
const pending = applications.deviceState.applyInProgress; const pending = applications.deviceState.applyInProgress;
const containerStates = (await applications.services.getAll()).map((svc) => const containerStates = (await applications.services.getAll()).map((svc) =>
@ -437,7 +438,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
router.get('/v2/device/name', async (_req, res) => { router.get('/v2/device/name', async (_req, res) => {
const deviceName = await applications.config.get('name'); const deviceName = await config.get('name');
res.json({ res.json({
status: 'success', status: 'success',
deviceName, deviceName,
@ -460,10 +461,10 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
router.get('/v2/device/vpn', async (_req, res) => { router.get('/v2/device/vpn', async (_req, res) => {
const config = await deviceState.deviceConfig.getCurrent(); const conf = await deviceState.deviceConfig.getCurrent();
// Build VPNInfo // Build VPNInfo
const info = { const info = {
enabled: config.SUPERVISOR_VPN_CONTROL === 'true', enabled: conf.SUPERVISOR_VPN_CONTROL === 'true',
connected: await isVPNActive(), connected: await isVPNActive(),
}; };
// Return payload // Return payload

View File

@ -1,7 +1,7 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import { inspect } from 'util'; import { inspect } from 'util';
import Config from './config'; import * as config from './config';
import { SchemaTypeKey } from './config/schema-type'; import { SchemaTypeKey } from './config/schema-type';
import * as db from './db'; import * as db from './db';
import Logger from './logger'; import Logger from './logger';
@ -17,7 +17,6 @@ import { DeviceStatus } from './types/state';
const vpnServiceName = 'openvpn'; const vpnServiceName = 'openvpn';
interface DeviceConfigConstructOpts { interface DeviceConfigConstructOpts {
config: Config;
logger: Logger; logger: Logger;
} }
@ -56,7 +55,6 @@ interface DeviceActionExecutors {
} }
export class DeviceConfig { export class DeviceConfig {
private config: Config;
private logger: Logger; private logger: Logger;
private rebootRequired = false; private rebootRequired = false;
private actionExecutors: DeviceActionExecutors; private actionExecutors: DeviceActionExecutors;
@ -148,8 +146,7 @@ export class DeviceConfig {
}, },
}; };
public constructor({ config, logger }: DeviceConfigConstructOpts) { public constructor({ logger }: DeviceConfigConstructOpts) {
this.config = config;
this.logger = logger; this.logger = logger;
this.actionExecutors = { this.actionExecutors = {
@ -163,7 +160,7 @@ export class DeviceConfig {
} }
// TODO: Change the typing of step so that the types automatically // TODO: Change the typing of step so that the types automatically
// work out and we don't need this cast to any // work out and we don't need this cast to any
await this.config.set(step.target as { [key in SchemaTypeKey]: any }); await config.set(step.target as { [key in SchemaTypeKey]: any });
if (step.humanReadableTarget) { if (step.humanReadableTarget) {
this.logger.logConfigChange(step.humanReadableTarget, { this.logger.logConfigChange(step.humanReadableTarget, {
success: true, success: true,
@ -219,7 +216,7 @@ export class DeviceConfig {
if (this.configBackend != null) { if (this.configBackend != null) {
return this.configBackend; return this.configBackend;
} }
const dt = await this.config.get('deviceType'); const dt = await config.get('deviceType');
this.configBackend = this.configBackend =
(await configUtils.initialiseConfigBackend(dt, { (await configUtils.initialiseConfigBackend(dt, {
logger: this.logger, logger: this.logger,
@ -249,7 +246,7 @@ export class DeviceConfig {
public async getTarget({ initial = false }: { initial?: boolean } = {}) { public async getTarget({ initial = false }: { initial?: boolean } = {}) {
const [unmanaged, [devConfig]] = await Promise.all([ const [unmanaged, [devConfig]] = await Promise.all([
this.config.get('unmanaged'), config.get('unmanaged'),
db.models('deviceConfig').select('targetValues'), db.models('deviceConfig').select('targetValues'),
]); ]);
@ -278,7 +275,7 @@ export class DeviceConfig {
} }
public async getCurrent() { public async getCurrent() {
const conf = await this.config.getMany( const conf = await config.getMany(
['deviceType'].concat(_.keys(DeviceConfig.configKeys)) as SchemaTypeKey[], ['deviceType'].concat(_.keys(DeviceConfig.configKeys)) as SchemaTypeKey[],
); );
@ -384,7 +381,7 @@ export class DeviceConfig {
let steps: ConfigStep[] = []; let steps: ConfigStep[] = [];
const { deviceType, unmanaged } = await this.config.getMany([ const { deviceType, unmanaged } = await config.getMany([
'deviceType', 'deviceType',
'unmanaged', 'unmanaged',
]); ]);
@ -408,9 +405,7 @@ export class DeviceConfig {
) { ) {
// Check that the difference is not due to the variable having an invalid // Check that the difference is not due to the variable having an invalid
// value set from the cloud // value set from the cloud
if ( if (config.valueIsValid(key as SchemaTypeKey, target[envVarName])) {
this.config.valueIsValid(key as SchemaTypeKey, target[envVarName])
) {
// Save the change if it is both valid and different // Save the change if it is both valid and different
changingValue = target[envVarName]; changingValue = target[envVarName];
} else { } else {
@ -544,10 +539,7 @@ export class DeviceConfig {
); );
// Ensure devices already have required overlays // Ensure devices already have required overlays
DeviceConfig.ensureRequiredOverlay( DeviceConfig.ensureRequiredOverlay(await config.get('deviceType'), conf);
await this.config.get('deviceType'),
conf,
);
try { try {
await backend.setBootConfig(conf); await backend.setBootConfig(conf);

View File

@ -8,7 +8,7 @@ import StrictEventEmitter from 'strict-event-emitter-types';
import prettyMs = require('pretty-ms'); import prettyMs = require('pretty-ms');
import Config, { ConfigType } from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
import EventTracker from './event-tracker'; import EventTracker from './event-tracker';
import Logger from './logger'; import Logger from './logger';
@ -98,7 +98,7 @@ function createDeviceStateRouter(deviceState: DeviceState) {
res: express.Response, res: express.Response,
action: DeviceStateStepTarget, action: DeviceStateStepTarget,
) => { ) => {
const override = await deviceState.config.get('lockOverride'); const override = await config.get('lockOverride');
const force = validation.checkTruthy(req.body.force) || override; const force = validation.checkTruthy(req.body.force) || override;
try { try {
const response = await deviceState.executeStepAction( const response = await deviceState.executeStepAction(
@ -130,7 +130,7 @@ function createDeviceStateRouter(deviceState: DeviceState) {
router.patch('/v1/device/host-config', (req, res) => router.patch('/v1/device/host-config', (req, res) =>
hostConfig hostConfig
.patch(req.body, deviceState.config) .patch(req.body)
.then(() => res.status(200).send('OK')) .then(() => res.status(200).send('OK'))
.catch((err) => .catch((err) =>
res.status(503).send(err?.message ?? err ?? 'Unknown error'), res.status(503).send(err?.message ?? err ?? 'Unknown error'),
@ -177,7 +177,6 @@ function createDeviceStateRouter(deviceState: DeviceState) {
} }
interface DeviceStateConstructOpts { interface DeviceStateConstructOpts {
config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
logger: Logger; logger: Logger;
apiBinder: APIBinder; apiBinder: APIBinder;
@ -219,7 +218,6 @@ type DeviceStateStep<T extends PossibleStepTargets> =
| ConfigStep; | ConfigStep;
export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) { export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) {
public config: Config;
public eventTracker: EventTracker; public eventTracker: EventTracker;
public logger: Logger; public logger: Logger;
@ -244,22 +242,14 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
public connected: boolean; public connected: boolean;
public router: express.Router; public router: express.Router;
constructor({ constructor({ eventTracker, logger, apiBinder }: DeviceStateConstructOpts) {
config,
eventTracker,
logger,
apiBinder,
}: DeviceStateConstructOpts) {
super(); super();
this.config = config;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.logger = logger; this.logger = logger;
this.deviceConfig = new DeviceConfig({ this.deviceConfig = new DeviceConfig({
config: this.config,
logger: this.logger, logger: this.logger,
}); });
this.applications = new ApplicationManager({ this.applications = new ApplicationManager({
config: this.config,
logger: this.logger, logger: this.logger,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
deviceState: this, deviceState: this,
@ -285,7 +275,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
} }
public async healthcheck() { public async healthcheck() {
const unmanaged = await this.config.get('unmanaged'); const unmanaged = await config.get('unmanaged');
// Don't have to perform checks for unmanaged // Don't have to perform checks for unmanaged
if (unmanaged) { if (unmanaged) {
@ -319,7 +309,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
} }
public async init() { public async init() {
this.config.on('change', (changedConfig) => { config.on('change', (changedConfig) => {
if (changedConfig.loggingEnabled != null) { if (changedConfig.loggingEnabled != null) {
this.logger.enable(changedConfig.loggingEnabled); this.logger.enable(changedConfig.loggingEnabled);
} }
@ -331,7 +321,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
} }
}); });
const conf = await this.config.getMany([ const conf = await config.getMany([
'initialConfigSaved', 'initialConfigSaved',
'listenPort', 'listenPort',
'apiSecret', 'apiSecret',
@ -376,7 +366,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
try { try {
await loadTargetFromFile(null, this); await loadTargetFromFile(null, this);
} finally { } finally {
await this.config.set({ targetStateSet: true }); await config.set({ targetStateSet: true });
} }
} else { } else {
log.debug('Skipping preloading'); log.debug('Skipping preloading');
@ -385,7 +375,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
// and we need to mark that the target state has been set so that // and we need to mark that the target state has been set so that
// the supervisor doesn't try to preload again if in the future target // the supervisor doesn't try to preload again if in the future target
// apps are empty again (which may happen with multi-app). // apps are empty again (which may happen with multi-app).
await this.config.set({ targetStateSet: true }); await config.set({ targetStateSet: true });
} }
} }
await this.triggerApplyTarget({ initial: true }); await this.triggerApplyTarget({ initial: true });
@ -395,8 +385,8 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
apiEndpoint, apiEndpoint,
connectivityCheckEnabled, connectivityCheckEnabled,
}: { }: {
apiEndpoint: ConfigType<'apiEndpoint'>; apiEndpoint: config.ConfigType<'apiEndpoint'>;
connectivityCheckEnabled: ConfigType<'connectivityCheckEnabled'>; connectivityCheckEnabled: config.ConfigType<'connectivityCheckEnabled'>;
}) { }) {
network.startConnectivityCheck( network.startConnectivityCheck(
apiEndpoint, apiEndpoint,
@ -405,7 +395,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
return (this.connected = connected); return (this.connected = connected);
}, },
); );
this.config.on('change', function (changedConfig) { config.on('change', function (changedConfig) {
if (changedConfig.connectivityCheckEnabled != null) { if (changedConfig.connectivityCheckEnabled != null) {
return network.enableConnectivityCheck( return network.enableConnectivityCheck(
changedConfig.connectivityCheckEnabled, changedConfig.connectivityCheckEnabled,
@ -425,7 +415,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
const devConf = await this.deviceConfig.getCurrent(); const devConf = await this.deviceConfig.getCurrent();
await this.deviceConfig.setTarget(devConf); await this.deviceConfig.setTarget(devConf);
await this.config.set({ initialConfigSaved: true }); await config.set({ initialConfigSaved: true });
} }
// We keep compatibility with the StrictEventEmitter types // We keep compatibility with the StrictEventEmitter types
@ -472,11 +462,11 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
globalEventBus.getInstance().emit('targetStateChanged', target); globalEventBus.getInstance().emit('targetStateChanged', target);
const apiEndpoint = await this.config.get('apiEndpoint'); const apiEndpoint = await config.get('apiEndpoint');
await this.usingWriteLockTarget(async () => { await this.usingWriteLockTarget(async () => {
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await this.config.set({ name: target.local.name }, trx); await config.set({ name: target.local.name }, trx);
await this.deviceConfig.setTarget(target.local.config, trx); await this.deviceConfig.setTarget(target.local.config, trx);
if (localSource || apiEndpoint == null) { if (localSource || apiEndpoint == null) {
@ -511,7 +501,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
return { return {
local: { local: {
name: await this.config.get('name'), name: await config.get('name'),
config: await this.deviceConfig.getTarget({ initial }), config: await this.deviceConfig.getTarget({ initial }),
apps: await this.applications.getTargetApps(), apps: await this.applications.getTargetApps(),
}, },
@ -540,7 +530,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
DeviceStatus & { local: { name: string } } DeviceStatus & { local: { name: string } }
> { > {
const [name, devConfig, apps, dependent] = await Promise.all([ const [name, devConfig, apps, dependent] = await Promise.all([
this.config.get('name'), config.get('name'),
this.deviceConfig.getCurrent(), this.deviceConfig.getCurrent(),
this.applications.getCurrentForComparison(), this.applications.getCurrentForComparison(),
this.applications.getDependentState(), this.applications.getDependentState(),

View File

@ -3,6 +3,7 @@ import { fs } from 'mz';
import { Image } from '../compose/images'; import { Image } from '../compose/images';
import DeviceState from '../device-state'; import DeviceState from '../device-state';
import * as config from '../config';
import constants = require('../lib/constants'); import constants = require('../lib/constants');
import { AppsJsonParseError, EISDIR, ENOENT } from '../lib/errors'; import { AppsJsonParseError, EISDIR, ENOENT } from '../lib/errors';
@ -94,7 +95,7 @@ export async function loadTargetFromFile(
// multiple applications is possible // multiple applications is possible
if (commitToPin != null && appToPin != null) { if (commitToPin != null && appToPin != null) {
log.debug('Device will be pinned'); log.debug('Device will be pinned');
await deviceState.config.set({ await config.set({
pinDevice: { pinDevice: {
commit: commitToPin, commit: commitToPin,
app: parseInt(appToPin, 10), app: parseInt(appToPin, 10),

View File

@ -5,7 +5,7 @@ import * as mkdirCb from 'mkdirp';
import { fs } from 'mz'; import { fs } from 'mz';
import * as path from 'path'; import * as path from 'path';
import Config from './config'; import * as config from './config';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import * as dbus from './lib/dbus'; import * as dbus from './lib/dbus';
import { ENOENT } from './lib/errors'; import { ENOENT } from './lib/errors';
@ -150,8 +150,8 @@ async function readHostname() {
return _.trim(hostnameData); return _.trim(hostnameData);
} }
async function setHostname(val: string, configModel: Config) { async function setHostname(val: string) {
await configModel.set({ hostname: val }); await config.set({ hostname: val });
await dbus.restartService('resin-hostname'); await dbus.restartService('resin-hostname');
} }
@ -168,14 +168,14 @@ export function get(): Bluebird<HostConfig> {
}); });
} }
export function patch(conf: HostConfig, configModel: Config): Bluebird<void> { export function patch(conf: HostConfig): Bluebird<void> {
const promises: Array<Promise<void>> = []; const promises: Array<Promise<void>> = [];
if (conf != null && conf.network != null) { if (conf != null && conf.network != null) {
if (conf.network.proxy != null) { if (conf.network.proxy != null) {
promises.push(setProxy(conf.network.proxy)); promises.push(setProxy(conf.network.proxy));
} }
if (conf.network.hostname != null) { if (conf.network.hostname != null) {
promises.push(setHostname(conf.network.hostname, configModel)); promises.push(setHostname(conf.network.hostname));
} }
} }
return Bluebird.all(promises).return(); return Bluebird.all(promises).return();

View File

@ -10,7 +10,7 @@ const mkdirpAsync = Bluebird.promisify(mkdirp);
const rimrafAsync = Bluebird.promisify(rimraf); const rimrafAsync = Bluebird.promisify(rimraf);
import { ApplicationManager } from '../application-manager'; import { ApplicationManager } from '../application-manager';
import Config from '../config'; import * as config from '../config';
import * as db from '../db'; import * as db from '../db';
import DeviceState from '../device-state'; import DeviceState from '../device-state';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
@ -106,7 +106,6 @@ export function convertLegacyAppsJson(appsArray: any[]): AppsJsonFormat {
} }
export async function normaliseLegacyDatabase( export async function normaliseLegacyDatabase(
config: Config,
application: ApplicationManager, application: ApplicationManager,
balenaApi: PinejsClientRequest, balenaApi: PinejsClientRequest,
) { ) {

View File

@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird';
import * as Docker from 'dockerode'; import * as Docker from 'dockerode';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Config from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import { SupervisorContainerNotFoundError } from './lib/errors'; import { SupervisorContainerNotFoundError } from './lib/errors';
@ -71,7 +71,6 @@ const SUPERVISOR_CONTAINER_NAME_FALLBACK = 'resin_supervisor';
*/ */
export class LocalModeManager { export class LocalModeManager {
public constructor( public constructor(
public config: Config,
public docker: Docker, public docker: Docker,
public logger: Logger, public logger: Logger,
private containerId: string | undefined = constants.containerId, private containerId: string | undefined = constants.containerId,
@ -82,7 +81,7 @@ export class LocalModeManager {
public async init() { public async init() {
// Setup a listener to catch state changes relating to local mode // Setup a listener to catch state changes relating to local mode
this.config.on('change', (changed) => { config.on('change', (changed) => {
if (changed.localMode != null) { if (changed.localMode != null) {
const local = changed.localMode || false; const local = changed.localMode || false;
@ -96,15 +95,15 @@ export class LocalModeManager {
// On startup, check if we're in unmanaged mode, // On startup, check if we're in unmanaged mode,
// as local mode needs to be set // as local mode needs to be set
let unmanagedLocalMode = false; let unmanagedLocalMode = false;
if (await this.config.get('unmanaged')) { if (await config.get('unmanaged')) {
log.info('Starting up in unmanaged mode, activating local mode'); log.info('Starting up in unmanaged mode, activating local mode');
await this.config.set({ localMode: true }); await config.set({ localMode: true });
unmanagedLocalMode = true; unmanagedLocalMode = true;
} }
const localMode = const localMode =
// short circuit the next get if we know we're in local mode // short circuit the next get if we know we're in local mode
unmanagedLocalMode || (await this.config.get('localMode')); unmanagedLocalMode || (await config.get('localMode'));
if (!localMode) { if (!localMode) {
// Remove any leftovers if necessary // Remove any leftovers if necessary

View File

@ -1,7 +1,7 @@
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Config, { ConfigType } from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import Docker from './lib/docker-utils'; import Docker from './lib/docker-utils';
@ -20,14 +20,13 @@ import * as globalEventBus from './event-bus';
import log from './lib/supervisor-console'; import log from './lib/supervisor-console';
interface LoggerSetupOptions { interface LoggerSetupOptions {
apiEndpoint: ConfigType<'apiEndpoint'>; apiEndpoint: config.ConfigType<'apiEndpoint'>;
uuid: ConfigType<'uuid'>; uuid: config.ConfigType<'uuid'>;
deviceApiKey: ConfigType<'deviceApiKey'>; deviceApiKey: config.ConfigType<'deviceApiKey'>;
unmanaged: ConfigType<'unmanaged'>; unmanaged: config.ConfigType<'unmanaged'>;
localMode: ConfigType<'localMode'>; localMode: config.ConfigType<'localMode'>;
enableLogs: boolean; enableLogs: boolean;
config: Config;
} }
type LogEventObject = Dictionary<any> | null; type LogEventObject = Dictionary<any> | null;
@ -58,7 +57,6 @@ export class Logger {
unmanaged, unmanaged,
enableLogs, enableLogs,
localMode, localMode,
config,
}: LoggerSetupOptions) { }: LoggerSetupOptions) {
this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey); this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey);
this.localBackend = new LocalLogBackend(); this.localBackend = new LocalLogBackend();
@ -221,21 +219,21 @@ export class Logger {
} }
public logConfigChange( public logConfigChange(
config: { [configName: string]: string }, conf: { [configName: string]: string },
{ success = false, err }: { success?: boolean; err?: Error } = {}, { success = false, err }: { success?: boolean; err?: Error } = {},
) { ) {
const obj: LogEventObject = { config }; const obj: LogEventObject = { conf };
let message: string; let message: string;
let eventName: string; let eventName: string;
if (success) { if (success) {
message = `Applied configuration change ${JSON.stringify(config)}`; message = `Applied configuration change ${JSON.stringify(conf)}`;
eventName = 'Apply config change success'; eventName = 'Apply config change success';
} else if (err != null) { } else if (err != null) {
message = `Error applying configuration change: ${err}`; message = `Error applying configuration change: ${err}`;
eventName = 'Apply config change error'; eventName = 'Apply config change error';
obj.error = err; obj.error = err;
} else { } else {
message = `Applying configuration change ${JSON.stringify(config)}`; message = `Applying configuration change ${JSON.stringify(conf)}`;
eventName = 'Apply config change in progress'; eventName = 'Apply config change in progress';
} }

View File

@ -16,6 +16,7 @@ import * as url from 'url';
import { log } from './lib/supervisor-console'; import { log } from './lib/supervisor-console';
import * as db from './db'; import * as db from './db';
import * as config from './config';
const mkdirpAsync = Promise.promisify(mkdirp); const mkdirpAsync = Promise.promisify(mkdirp);
@ -201,7 +202,7 @@ const createProxyvisorRouter = function (proxyvisor) {
commit, commit,
releaseId, releaseId,
environment, environment,
config, config: conf,
} = req.body; } = req.body;
const validateDeviceFields = function () { const validateDeviceFields = function () {
if (isDefined(is_online) && !_.isBoolean(is_online)) { if (isDefined(is_online) && !_.isBoolean(is_online)) {
@ -219,7 +220,7 @@ const createProxyvisorRouter = function (proxyvisor) {
if (!validObjectOrUndefined(environment)) { if (!validObjectOrUndefined(environment)) {
return 'environment must be an object'; return 'environment must be an object';
} }
if (!validObjectOrUndefined(config)) { if (!validObjectOrUndefined(conf)) {
return 'config must be an object'; return 'config must be an object';
} }
return null; return null;
@ -233,12 +234,12 @@ const createProxyvisorRouter = function (proxyvisor) {
if (isDefined(environment)) { if (isDefined(environment)) {
environment = JSON.stringify(environment); environment = JSON.stringify(environment);
} }
if (isDefined(config)) { if (isDefined(conf)) {
config = JSON.stringify(config); conf = JSON.stringify(conf);
} }
const fieldsToUpdateOnDB = _.pickBy( const fieldsToUpdateOnDB = _.pickBy(
{ status, is_online, commit, releaseId, config, environment }, { status, is_online, commit, releaseId, config: conf, environment },
isDefined, isDefined,
); );
/** @type {Dictionary<any>} */ /** @type {Dictionary<any>} */
@ -343,7 +344,7 @@ const createProxyvisorRouter = function (proxyvisor) {
}; };
export class Proxyvisor { export class Proxyvisor {
constructor({ config, logger, docker, images, applications }) { constructor({ logger, docker, images, applications }) {
this.bindToAPI = this.bindToAPI.bind(this); this.bindToAPI = this.bindToAPI.bind(this);
this.executeStepAction = this.executeStepAction.bind(this); this.executeStepAction = this.executeStepAction.bind(this);
this.getCurrentStates = this.getCurrentStates.bind(this); this.getCurrentStates = this.getCurrentStates.bind(this);
@ -359,7 +360,6 @@ export class Proxyvisor {
this.sendUpdate = this.sendUpdate.bind(this); this.sendUpdate = this.sendUpdate.bind(this);
this.sendDeleteHook = this.sendDeleteHook.bind(this); this.sendDeleteHook = this.sendDeleteHook.bind(this);
this.sendUpdates = this.sendUpdates.bind(this); this.sendUpdates = this.sendUpdates.bind(this);
this.config = config;
this.logger = logger; this.logger = logger;
this.docker = docker; this.docker = docker;
this.images = images; this.images = images;
@ -369,7 +369,7 @@ export class Proxyvisor {
this.router = createProxyvisorRouter(this); this.router = createProxyvisorRouter(this);
this.actionExecutors = { this.actionExecutors = {
updateDependentTargets: (step) => { updateDependentTargets: (step) => {
return this.config return config
.getMany(['currentApiKey', 'apiTimeout']) .getMany(['currentApiKey', 'apiTimeout'])
.then(({ currentApiKey, apiTimeout }) => { .then(({ currentApiKey, apiTimeout }) => {
// - take each of the step.devices and update dependentDevice with it (targetCommit, targetEnvironment, targetConfig) // - take each of the step.devices and update dependentDevice with it (targetCommit, targetEnvironment, targetConfig)
@ -446,7 +446,7 @@ export class Proxyvisor {
sendDependentHooks: (step) => { sendDependentHooks: (step) => {
return Promise.join( return Promise.join(
this.config.get('apiTimeout'), config.get('apiTimeout'),
this.getHookEndpoint(step.appId), this.getHookEndpoint(step.appId),
(apiTimeout, endpoint) => { (apiTimeout, endpoint) => {
return Promise.mapSeries(step.devices, (device) => { return Promise.mapSeries(step.devices, (device) => {
@ -965,7 +965,7 @@ export class Proxyvisor {
sendUpdates({ uuid }) { sendUpdates({ uuid }) {
return Promise.join( return Promise.join(
db.models('dependentDevice').where({ uuid }).select(), db.models('dependentDevice').where({ uuid }).select(),
this.config.get('apiTimeout'), config.get('apiTimeout'),
([dev], apiTimeout) => { ([dev], apiTimeout) => {
if (dev == null) { if (dev == null) {
log.warn(`Trying to send update to non-existent device ${uuid}`); log.warn(`Trying to send update to non-existent device ${uuid}`);

View File

@ -4,7 +4,7 @@ import { Server } from 'http';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as morgan from 'morgan'; import * as morgan from 'morgan';
import Config from './config'; import * as config from './config';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import blink = require('./lib/blink'); import blink = require('./lib/blink');
import * as iptables from './lib/iptables'; import * as iptables from './lib/iptables';
@ -29,7 +29,7 @@ function getKeyFromReq(req: express.Request): string | undefined {
return match?.[1]; return match?.[1];
} }
function authenticate(config: Config): express.RequestHandler { function authenticate(): express.RequestHandler {
return async (req, res, next) => { return async (req, res, next) => {
try { try {
const conf = await config.getMany([ const conf = await config.getMany([
@ -76,7 +76,6 @@ const expressLogger = morgan(
); );
interface SupervisorAPIConstructOpts { interface SupervisorAPIConstructOpts {
config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
routers: express.Router[]; routers: express.Router[];
healthchecks: Array<() => Promise<boolean>>; healthchecks: Array<() => Promise<boolean>>;
@ -87,7 +86,6 @@ interface SupervisorAPIStopOpts {
} }
export class SupervisorAPI { export class SupervisorAPI {
private config: Config;
private eventTracker: EventTracker; private eventTracker: EventTracker;
private routers: express.Router[]; private routers: express.Router[];
private healthchecks: Array<() => Promise<boolean>>; private healthchecks: Array<() => Promise<boolean>>;
@ -104,12 +102,10 @@ export class SupervisorAPI {
: this.applyListeningRules.bind(this); : this.applyListeningRules.bind(this);
public constructor({ public constructor({
config,
eventTracker, eventTracker,
routers, routers,
healthchecks, healthchecks,
}: SupervisorAPIConstructOpts) { }: SupervisorAPIConstructOpts) {
this.config = config;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.routers = routers; this.routers = routers;
this.healthchecks = healthchecks; this.healthchecks = healthchecks;
@ -133,7 +129,7 @@ export class SupervisorAPI {
this.api.get('/ping', (_req, res) => res.send('OK')); this.api.get('/ping', (_req, res) => res.send('OK'));
this.api.use(authenticate(this.config)); this.api.use(authenticate());
this.api.post('/v1/blink', (_req, res) => { this.api.post('/v1/blink', (_req, res) => {
this.eventTracker.track('Device blink'); this.eventTracker.track('Device blink');
@ -145,8 +141,8 @@ export class SupervisorAPI {
// Expires the supervisor's API key and generates a new one. // Expires the supervisor's API key and generates a new one.
// It also communicates the new key to the balena API. // It also communicates the new key to the balena API.
this.api.post('/v1/regenerate-api-key', async (_req, res) => { this.api.post('/v1/regenerate-api-key', async (_req, res) => {
const secret = await this.config.newUniqueKey(); const secret = config.newUniqueKey();
await this.config.set({ apiSecret: secret }); await config.set({ apiSecret: secret });
res.status(200).send(secret); res.status(200).send(secret);
}); });
@ -190,11 +186,11 @@ export class SupervisorAPI {
port: number, port: number,
apiTimeout: number, apiTimeout: number,
): Promise<void> { ): Promise<void> {
const localMode = await this.config.get('localMode'); const localMode = await config.get('localMode');
await this.applyRules(localMode || false, port, allowedInterfaces); await this.applyRules(localMode || false, port, allowedInterfaces);
// Monitor the switching of local mode, and change which interfaces will // Monitor the switching of local mode, and change which interfaces will
// be listened to based on that // be listened to based on that
this.config.on('change', (changedConfig) => { config.on('change', (changedConfig) => {
if (changedConfig.localMode != null) { if (changedConfig.localMode != null) {
this.applyRules( this.applyRules(
changedConfig.localMode || false, changedConfig.localMode || false,

View File

@ -1,6 +1,6 @@
import APIBinder from './api-binder'; import APIBinder from './api-binder';
import Config, { ConfigKey } from './config';
import * as db from './db'; import * as db from './db';
import * as config from './config';
import DeviceState from './device-state'; import DeviceState from './device-state';
import EventTracker from './event-tracker'; import EventTracker from './event-tracker';
import { intialiseContractRequirements } from './lib/contracts'; import { intialiseContractRequirements } from './lib/contracts';
@ -13,7 +13,7 @@ import constants = require('./lib/constants');
import log from './lib/supervisor-console'; import log from './lib/supervisor-console';
import version = require('./lib/supervisor-version'); import version = require('./lib/supervisor-version');
const startupConfigFields: ConfigKey[] = [ const startupConfigFields: config.ConfigKey[] = [
'uuid', 'uuid',
'listenPort', 'listenPort',
'apiEndpoint', 'apiEndpoint',
@ -29,7 +29,6 @@ const startupConfigFields: ConfigKey[] = [
]; ];
export class Supervisor { export class Supervisor {
private config: Config;
private eventTracker: EventTracker; private eventTracker: EventTracker;
private logger: Logger; private logger: Logger;
private deviceState: DeviceState; private deviceState: DeviceState;
@ -37,16 +36,13 @@ export class Supervisor {
private api: SupervisorAPI; private api: SupervisorAPI;
public constructor() { public constructor() {
this.config = new Config();
this.eventTracker = new EventTracker(); this.eventTracker = new EventTracker();
this.logger = new Logger({ eventTracker: this.eventTracker }); this.logger = new Logger({ eventTracker: this.eventTracker });
this.apiBinder = new APIBinder({ this.apiBinder = new APIBinder({
config: this.config,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
logger: this.logger, logger: this.logger,
}); });
this.deviceState = new DeviceState({ this.deviceState = new DeviceState({
config: this.config,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
logger: this.logger, logger: this.logger,
apiBinder: this.apiBinder, apiBinder: this.apiBinder,
@ -59,7 +55,6 @@ export class Supervisor {
this.deviceState.applications.proxyvisor.bindToAPI(this.apiBinder); this.deviceState.applications.proxyvisor.bindToAPI(this.apiBinder);
this.api = new SupervisorAPI({ this.api = new SupervisorAPI({
config: this.config,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
routers: [this.apiBinder.router, this.deviceState.router], routers: [this.apiBinder.router, this.deviceState.router],
healthchecks: [ healthchecks: [
@ -73,9 +68,9 @@ export class Supervisor {
log.info(`Supervisor v${version} starting up...`); log.info(`Supervisor v${version} starting up...`);
await db.initialized; await db.initialized;
await this.config.init(); await config.initialized;
const conf = await this.config.getMany(startupConfigFields); const conf = await config.getMany(startupConfigFields);
// We can't print to the dashboard until the logger // We can't print to the dashboard until the logger
// has started up, so we leave a trail of breadcrumbs // has started up, so we leave a trail of breadcrumbs
@ -87,13 +82,12 @@ export class Supervisor {
log.debug('Starting logging infrastructure'); log.debug('Starting logging infrastructure');
this.logger.init({ this.logger.init({
enableLogs: conf.loggingEnabled, enableLogs: conf.loggingEnabled,
config: this.config,
...conf, ...conf,
}); });
intialiseContractRequirements({ intialiseContractRequirements({
supervisorVersion: version, supervisorVersion: version,
deviceType: await this.config.get('deviceType'), deviceType: await config.get('deviceType'),
l4tVersion: await osRelease.getL4tVersion(), l4tVersion: await osRelease.getL4tVersion(),
}); });
@ -104,7 +98,6 @@ export class Supervisor {
if (conf.legacyAppsPresent && this.apiBinder.balenaApi != null) { if (conf.legacyAppsPresent && this.apiBinder.balenaApi != null) {
log.info('Legacy app detected, running migration'); log.info('Legacy app detected, running migration');
await normaliseLegacyDatabase( await normaliseLegacyDatabase(
this.deviceState.config,
this.deviceState.applications, this.deviceState.applications,
this.apiBinder.balenaApi, this.apiBinder.balenaApi,
); );

View File

@ -1,7 +1,7 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ApplicationManager } from './application-manager'; import { ApplicationManager } from './application-manager';
import Config from './config'; import * as config from './config';
import * as db from './db'; import * as db from './db';
// Once we have correct types for both applications and the // Once we have correct types for both applications and the
@ -23,14 +23,11 @@ export type DatabaseApps = DatabaseApp[];
export class TargetStateAccessor { export class TargetStateAccessor {
private targetState?: DatabaseApps; private targetState?: DatabaseApps;
public constructor( public constructor(protected applications: ApplicationManager) {
protected applications: ApplicationManager,
protected config: Config,
) {
// If we switch backend, the target state also needs to // If we switch backend, the target state also needs to
// be invalidated (this includes switching to and from // be invalidated (this includes switching to and from
// local mode) // local mode)
this.config.on('change', (conf) => { config.on('change', (conf) => {
if (conf.apiEndpoint != null || conf.localMode != null) { if (conf.apiEndpoint != null || conf.localMode != null) {
this.targetState = undefined; this.targetState = undefined;
} }
@ -49,7 +46,7 @@ export class TargetStateAccessor {
public async getTargetApps(): Promise<DatabaseApps> { public async getTargetApps(): Promise<DatabaseApps> {
if (this.targetState == null) { if (this.targetState == null) {
const { apiEndpoint, localMode } = await this.config.getMany([ const { apiEndpoint, localMode } = await config.getMany([
'apiEndpoint', 'apiEndpoint',
'localMode', 'localMode',
]); ]);

View File

@ -28,6 +28,10 @@ try {
} catch { } catch {
/* noop */ /* noop */
} }
fs.writeFileSync(
'./test/data/config.json',
fs.readFileSync('./test/data/testconfig.json'),
);
stub(dbus, 'getBus').returns({ stub(dbus, 'getBus').returns({
getInterface: ( getInterface: (

View File

@ -3,37 +3,27 @@ import { fs } from 'mz';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
import * as conf from '../src/config';
import constants = require('../src/lib/constants');
import { SchemaTypeKey } from '../src/config/schema-type';
// tslint:disable-next-line // tslint:disable-next-line
chai.use(require('chai-events')); chai.use(require('chai-events'));
const { expect } = chai; const { expect } = chai;
import Config from '../src/config';
import constants = require('../src/lib/constants');
describe('Config', () => { describe('Config', () => {
let conf: Config;
before(async () => { before(async () => {
await prepare(); await prepare();
conf = new Config(); await conf.initialized;
await conf.init();
}); });
it('uses the correct config.json path', async () => { it('uses the correct config.json path', async () => {
expect(await (conf as any).configJsonBackend.path()).to.equal( expect(await conf.configJsonBackend.path()).to.equal(
'test/data/config.json', 'test/data/config.json',
); );
}); });
it('uses the correct config.json path from the root mount when passed as argument to the constructor', async () => {
const conf2 = new Config({ configPath: '/foo.json' });
expect(await (conf2 as any).configJsonBackend.path()).to.equal(
'test/data/foo.json',
);
});
it('reads and exposes values from the config.json', async () => { it('reads and exposes values from the config.json', async () => {
const id = await conf.get('applicationId'); const id = await conf.get('applicationId');
return expect(id).to.equal(78373); return expect(id).to.equal(78373);
@ -107,13 +97,20 @@ describe('Config', () => {
expect(conf.get('unknownInvalidValue' as any)).to.be.rejected; expect(conf.get('unknownInvalidValue' as any)).to.be.rejected;
}); });
it('emits a change event when values are set', (done) => { it('emits a change event when values', (done) => {
conf.on('change', (val) => { const listener = (val: conf.ConfigChangeMap<SchemaTypeKey>) => {
expect(val).to.deep.equal({ name: 'someValue' }); try {
return done(); if ('name' in val) {
}); expect(val.name).to.equal('someValue');
done();
conf.removeListener('change', listener);
}
} catch (e) {
done(e);
}
};
conf.on('change', listener);
conf.set({ name: 'someValue' }); conf.set({ name: 'someValue' });
(expect(conf).to as any).emit('change');
}); });
it("returns an undefined OS variant if it doesn't exist", async () => { it("returns an undefined OS variant if it doesn't exist", async () => {
@ -126,12 +123,6 @@ describe('Config', () => {
}); });
describe('Function config providers', () => { describe('Function config providers', () => {
before(async () => {
await prepare();
conf = new Config();
await conf.init();
});
it('should throw if a non-mutable function provider is set', () => { it('should throw if a non-mutable function provider is set', () => {
expect(conf.set({ version: 'some-version' })).to.be.rejected; expect(conf.set({ version: 'some-version' })).to.be.rejected;
}); });

View File

@ -6,7 +6,7 @@ import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
import Log from '../src/lib/supervisor-console'; import Log from '../src/lib/supervisor-console';
import Config from '../src/config'; import * as config from '../src/config';
import { RPiConfigBackend } from '../src/config/backend'; import { RPiConfigBackend } from '../src/config/backend';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
import { loadTargetFromFile } from '../src/device-state/preload'; import { loadTargetFromFile } from '../src/device-state/preload';
@ -207,7 +207,6 @@ const testTargetInvalid = {
}; };
describe('deviceState', () => { describe('deviceState', () => {
const config = new Config();
const logger = { const logger = {
clearOutOfDateDBLogs() { clearOutOfDateDBLogs() {
/* noop */ /* noop */
@ -231,7 +230,6 @@ describe('deviceState', () => {
}); });
deviceState = new DeviceState({ deviceState = new DeviceState({
config,
eventTracker: eventTracker as any, eventTracker: eventTracker as any,
logger: logger as any, logger: logger as any,
apiBinder: null as any, apiBinder: null as any,
@ -248,7 +246,6 @@ describe('deviceState', () => {
}); });
(deviceState as any).deviceConfig.configBackend = new RPiConfigBackend(); (deviceState as any).deviceConfig.configBackend = new RPiConfigBackend();
await config.init();
}); });
after(() => { after(() => {
@ -391,7 +388,7 @@ describe('deviceState', () => {
beforeEach(() => { beforeEach(() => {
// This configStub will be modified in each test case so we can // This configStub will be modified in each test case so we can
// create the exact conditions we want to for testing healthchecks // create the exact conditions we want to for testing healthchecks
configStub = stub(Config.prototype, 'get'); configStub = stub(config, 'get');
infoLobSpy = spy(Log, 'info'); infoLobSpy = spy(Log, 'info');
}); });

View File

@ -4,19 +4,28 @@ import { Server } from 'net';
import { SinonSpy, SinonStub, spy, stub } from 'sinon'; import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import ApiBinder from '../src/api-binder'; import ApiBinder from '../src/api-binder';
import Config from '../src/config'; import prepare = require('./lib/prepare');
import * as config from '../src/config';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
import Log from '../src/lib/supervisor-console'; import Log from '../src/lib/supervisor-console';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import balenaAPI = require('./lib/mocked-balena-api'); import balenaAPI = require('./lib/mocked-balena-api');
import prepare = require('./lib/prepare'); import { schema } from '../src/config/schema';
import ConfigJsonConfigBackend from '../src/config/configJson';
const { expect } = chai; const { expect } = chai;
const defaultConfigBackend = config.configJsonBackend;
const initModels = async (obj: Dictionary<any>, filename: string) => { const initModels = async (obj: Dictionary<any>, filename: string) => {
await prepare(); await prepare();
config.removeAllListeners();
obj.config = new Config({ configPath: filename }); // @ts-ignore
config.configJsonBackend = new ConfigJsonConfigBackend(schema, filename);
config.generateRequiredFields();
// @ts-expect-error using private properties
config.configJsonBackend.cache = await config.configJsonBackend.read();
await config.generateRequiredFields();
obj.eventTracker = { obj.eventTracker = {
track: stub().callsFake((ev, props) => console.log(ev, props)), track: stub().callsFake((ev, props) => console.log(ev, props)),
@ -29,13 +38,11 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
} as any; } as any;
obj.apiBinder = new ApiBinder({ obj.apiBinder = new ApiBinder({
config: obj.config,
logger: obj.logger, logger: obj.logger,
eventTracker: obj.eventTracker, eventTracker: obj.eventTracker,
}); });
obj.deviceState = new DeviceState({ obj.deviceState = new DeviceState({
config: obj.config,
eventTracker: obj.eventTracker, eventTracker: obj.eventTracker,
logger: obj.logger, logger: obj.logger,
apiBinder: obj.apiBinder, apiBinder: obj.apiBinder,
@ -43,7 +50,6 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
obj.apiBinder.setDeviceState(obj.deviceState); obj.apiBinder.setDeviceState(obj.deviceState);
await obj.config.init();
await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning
}; };
@ -80,6 +86,12 @@ describe('ApiBinder', () => {
return initModels(components, '/config-apibinder.json'); return initModels(components, '/config-apibinder.json');
}); });
after(async () => {
// @ts-ignore
config.configJsonBackend = defaultConfigBackend;
await config.generateRequiredFields();
});
it('provisions a device', () => { it('provisions a device', () => {
// @ts-ignore // @ts-ignore
const promise = components.apiBinder.provisionDevice(); const promise = components.apiBinder.provisionDevice();
@ -97,11 +109,9 @@ describe('ApiBinder', () => {
it('exchanges keys if resource conflict when provisioning', async () => { it('exchanges keys if resource conflict when provisioning', async () => {
// Get current config to extend // Get current config to extend
const currentConfig = await components.apiBinder.config.get( const currentConfig = await config.get('provisioningOptions');
'provisioningOptions',
);
// Stub config values so we have correct conditions // Stub config values so we have correct conditions
const configStub = stub(Config.prototype, 'get').resolves({ const configStub = stub(config, 'get').resolves({
...currentConfig, ...currentConfig,
registered_at: null, registered_at: null,
provisioningApiKey: '123', // Previous test case deleted the provisioningApiKey so add one provisioningApiKey: '123', // Previous test case deleted the provisioningApiKey so add one
@ -135,7 +145,7 @@ describe('ApiBinder', () => {
}); });
it('deletes the provisioning key', async () => { it('deletes the provisioning key', async () => {
expect(await components.config.get('apiKey')).to.be.undefined; expect(await config.get('apiKey')).to.be.undefined;
}); });
it('sends the correct parameters when provisioning', async () => { it('sends the correct parameters when provisioning', async () => {
@ -159,6 +169,11 @@ describe('ApiBinder', () => {
before(() => { before(() => {
return initModels(components, '/config-apibinder.json'); return initModels(components, '/config-apibinder.json');
}); });
after(async () => {
// @ts-ignore
config.configJsonBackend = defaultConfigBackend;
await config.generateRequiredFields();
});
it('gets a device by its uuid from the balena API', async () => { it('gets a device by its uuid from the balena API', async () => {
// Manually add a device to the mocked API // Manually add a device to the mocked API
@ -185,6 +200,11 @@ describe('ApiBinder', () => {
before(() => { before(() => {
return initModels(components, '/config-apibinder.json'); return initModels(components, '/config-apibinder.json');
}); });
after(async () => {
// @ts-ignore
config.configJsonBackend = defaultConfigBackend;
await config.generateRequiredFields();
});
it('returns the device if it can fetch it with the deviceApiKey', async () => { it('returns the device if it can fetch it with the deviceApiKey', async () => {
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler'); spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
@ -254,13 +274,18 @@ describe('ApiBinder', () => {
before(() => { before(() => {
return initModels(components, '/config-apibinder-offline.json'); return initModels(components, '/config-apibinder-offline.json');
}); });
after(async () => {
// @ts-ignore
config.configJsonBackend = defaultConfigBackend;
await config.generateRequiredFields();
});
it('does not generate a key if the device is in unmanaged mode', async () => { it('does not generate a key if the device is in unmanaged mode', async () => {
const mode = await components.config.get('unmanaged'); const mode = await config.get('unmanaged');
// Ensure offline mode is set // Ensure offline mode is set
expect(mode).to.equal(true); expect(mode).to.equal(true);
// Check that there is no deviceApiKey // Check that there is no deviceApiKey
const conf = await components.config.getMany(['deviceApiKey', 'uuid']); const conf = await config.getMany(['deviceApiKey', 'uuid']);
expect(conf['deviceApiKey']).to.be.empty; expect(conf['deviceApiKey']).to.be.empty;
expect(conf['uuid']).to.not.be.undefined; expect(conf['uuid']).to.not.be.undefined;
}); });
@ -272,9 +297,9 @@ describe('ApiBinder', () => {
}); });
it('does not generate a key with the minimal config', async () => { it('does not generate a key with the minimal config', async () => {
const mode = await components2.config.get('unmanaged'); const mode = await config.get('unmanaged');
expect(mode).to.equal(true); expect(mode).to.equal(true);
const conf = await components2.config.getMany(['deviceApiKey', 'uuid']); const conf = await config.getMany(['deviceApiKey', 'uuid']);
expect(conf['deviceApiKey']).to.be.empty; expect(conf['deviceApiKey']).to.be.empty;
return expect(conf['uuid']).to.not.be.undefined; return expect(conf['uuid']).to.not.be.undefined;
}); });
@ -289,11 +314,16 @@ describe('ApiBinder', () => {
before(async () => { before(async () => {
await initModels(components, '/config-apibinder.json'); await initModels(components, '/config-apibinder.json');
}); });
after(async () => {
// @ts-ignore
config.configJsonBackend = defaultConfigBackend;
await config.generateRequiredFields();
});
beforeEach(() => { beforeEach(() => {
// This configStub will be modified in each test case so we can // This configStub will be modified in each test case so we can
// create the exact conditions we want to for testing healthchecks // create the exact conditions we want to for testing healthchecks
configStub = stub(Config.prototype, 'getMany'); configStub = stub(config, 'getMany');
infoLobSpy = spy(Log, 'info'); infoLobSpy = spy(Log, 'info');
}); });

View File

@ -3,6 +3,7 @@ import { stripIndent } from 'common-tags';
import { child_process, fs } from 'mz'; import { child_process, fs } from 'mz';
import { SinonSpy, SinonStub, spy, stub } from 'sinon'; import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import * as config from '../src/config';
import { ExtlinuxConfigBackend, RPiConfigBackend } from '../src/config/backend'; import { ExtlinuxConfigBackend, RPiConfigBackend } from '../src/config/backend';
import { DeviceConfig } from '../src/device-config'; import { DeviceConfig } from '../src/device-config';
import * as fsUtils from '../src/lib/fs-utils'; import * as fsUtils from '../src/lib/fs-utils';
@ -32,7 +33,6 @@ describe('DeviceConfig', function () {
}; };
return (this.deviceConfig = new DeviceConfig({ return (this.deviceConfig = new DeviceConfig({
logger: this.fakeLogger, logger: this.fakeLogger,
config: this.fakeConfig,
})); }));
}); });
@ -397,19 +397,16 @@ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\
describe('ConfigFS', function () { describe('ConfigFS', function () {
before(function () { before(function () {
const fakeConfig = { stub(config, 'get').callsFake((key) => {
get(key: string) { return Promise.try(function () {
return Promise.try(function () { if (key === 'deviceType') {
if (key === 'deviceType') { return 'up-board';
return 'up-board'; }
} throw new Error('Unknown fake config key');
throw new Error('Unknown fake config key'); });
}); });
},
};
this.upboardConfig = new DeviceConfig({ this.upboardConfig = new DeviceConfig({
logger: this.fakeLogger, logger: this.fakeLogger,
config: fakeConfig as any,
}); });
stub(child_process, 'exec').resolves(); stub(child_process, 'exec').resolves();
@ -440,6 +437,17 @@ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\
}); });
}); });
after(function () {
(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();
this.fakeLogger.logSystemMessage.resetHistory();
});
it('should correctly load the configfs.json file', function () { it('should correctly load the configfs.json file', function () {
expect(child_process.exec).to.be.calledWith('modprobe acpi_configfs'); expect(child_process.exec).to.be.calledWith('modprobe acpi_configfs');
expect(child_process.exec).to.be.calledWith( expect(child_process.exec).to.be.calledWith(
@ -491,16 +499,6 @@ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\
).to.equal('Apply boot config success'); ).to.equal('Apply boot config success');
}); });
}); });
return after(function () {
(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();
return this.fakeLogger.logSystemMessage.resetHistory();
});
}); });
// This will require stubbing device.reboot, gosuper.post, config.get/set // This will require stubbing device.reboot, gosuper.post, config.get/set

View File

@ -2,8 +2,6 @@ import * as Bluebird from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { stub } from 'sinon'; import { stub } from 'sinon';
import Config from '../src/config';
import Network from '../src/compose/network'; import Network from '../src/compose/network';
import Service from '../src/compose/service'; import Service from '../src/compose/service';
@ -127,7 +125,6 @@ const dependentDBFormat = {
describe('ApplicationManager', function () { describe('ApplicationManager', function () {
before(async function () { before(async function () {
await prepare(); await prepare();
this.config = new Config();
const eventTracker = new EventTracker(); const eventTracker = new EventTracker();
this.logger = { this.logger = {
clearOutOfDateDBLogs: () => { clearOutOfDateDBLogs: () => {
@ -135,7 +132,6 @@ describe('ApplicationManager', function () {
}, },
} as any; } as any;
this.deviceState = new DeviceState({ this.deviceState = new DeviceState({
config: this.config,
eventTracker, eventTracker,
logger: this.logger, logger: this.logger,
apiBinder: null as any, apiBinder: null as any,
@ -226,7 +222,6 @@ describe('ApplicationManager', function () {
return targetCloned; return targetCloned;
}); });
}; };
return this.config.init();
}); });
beforeEach( beforeEach(

View File

@ -3,7 +3,6 @@ import { expect } from 'chai';
import * as Docker from 'dockerode'; import * as Docker from 'dockerode';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import Config from '../src/config';
import * as db from '../src/db'; import * as db from '../src/db';
import LocalModeManager, { import LocalModeManager, {
EngineSnapshot, EngineSnapshot,
@ -34,11 +33,9 @@ describe('LocalModeManager', () => {
await db.initialized; await db.initialized;
dockerStub = sinon.createStubInstance(Docker); dockerStub = sinon.createStubInstance(Docker);
const configStub = (sinon.createStubInstance(Config) as unknown) as Config;
const loggerStub = (sinon.createStubInstance(Logger) as unknown) as Logger; const loggerStub = (sinon.createStubInstance(Logger) as unknown) as Logger;
localMode = new LocalModeManager( localMode = new LocalModeManager(
configStub,
(dockerStub as unknown) as Docker, (dockerStub as unknown) as Docker,
loggerStub, loggerStub,
supervisorContainerId, supervisorContainerId,

View File

@ -7,7 +7,7 @@ import { Images } from '../../src/compose/images';
import { NetworkManager } from '../../src/compose/network-manager'; import { NetworkManager } from '../../src/compose/network-manager';
import { ServiceManager } from '../../src/compose/service-manager'; import { ServiceManager } from '../../src/compose/service-manager';
import { VolumeManager } from '../../src/compose/volume-manager'; import { VolumeManager } from '../../src/compose/volume-manager';
import Config from '../../src/config'; import * as config from '../../src/config';
import * as db from '../../src/db'; import * as db from '../../src/db';
import { createV1Api } from '../../src/device-api/v1'; import { createV1Api } from '../../src/device-api/v1';
import { createV2Api } from '../../src/device-api/v2'; import { createV2Api } from '../../src/device-api/v2';
@ -68,17 +68,11 @@ const STUBBED_VALUES = {
async function create(): Promise<SupervisorAPI> { async function create(): Promise<SupervisorAPI> {
// Get SupervisorAPI construct options // Get SupervisorAPI construct options
const { const { eventTracker, deviceState, apiBinder } = await createAPIOpts();
config,
eventTracker,
deviceState,
apiBinder,
} = await createAPIOpts();
// Stub functions // Stub functions
setupStubs(); setupStubs();
// Create ApplicationManager // Create ApplicationManager
const appManager = new ApplicationManager({ const appManager = new ApplicationManager({
config,
eventTracker, eventTracker,
logger: null, logger: null,
deviceState, deviceState,
@ -86,7 +80,6 @@ async function create(): Promise<SupervisorAPI> {
}); });
// Create SupervisorAPI // Create SupervisorAPI
const api = new SupervisorAPI({ const api = new SupervisorAPI({
config,
eventTracker, eventTracker,
routers: [buildRoutes(appManager)], routers: [buildRoutes(appManager)],
healthchecks: [deviceState.healthcheck, apiBinder.healthcheck], healthchecks: [deviceState.healthcheck, apiBinder.healthcheck],
@ -108,33 +101,30 @@ async function cleanUp(): Promise<void> {
async function createAPIOpts(): Promise<SupervisorAPIOpts> { async function createAPIOpts(): Promise<SupervisorAPIOpts> {
await db.initialized; await db.initialized;
// Create config
const mockedConfig = new Config();
// Initialize and set values for mocked Config // Initialize and set values for mocked Config
await initConfig(mockedConfig); await initConfig();
// Create EventTracker // Create EventTracker
const tracker = new EventTracker(); const tracker = new EventTracker();
// Create deviceState // Create deviceState
const deviceState = new DeviceState({ const deviceState = new DeviceState({
config: mockedConfig,
eventTracker: tracker, eventTracker: tracker,
logger: null as any, logger: null as any,
apiBinder: null as any, apiBinder: null as any,
}); });
const apiBinder = new APIBinder({ const apiBinder = new APIBinder({
config: mockedConfig,
eventTracker: tracker, eventTracker: tracker,
logger: null as any, logger: null as any,
}); });
return { return {
config: mockedConfig,
eventTracker: tracker, eventTracker: tracker,
deviceState, deviceState,
apiBinder, apiBinder,
}; };
} }
async function initConfig(config: Config): Promise<void> { async function initConfig(): Promise<void> {
// Initialize this config
await config.initialized;
// Set testing secret // Set testing secret
await config.set({ await config.set({
apiSecret: STUBBED_VALUES.config.apiSecret, apiSecret: STUBBED_VALUES.config.apiSecret,
@ -143,8 +133,6 @@ async function initConfig(config: Config): Promise<void> {
await config.set({ await config.set({
currentCommit: STUBBED_VALUES.config.currentCommit, currentCommit: STUBBED_VALUES.config.currentCommit,
}); });
// Initialize this config
return config.init();
} }
function buildRoutes(appManager: ApplicationManager): Router { function buildRoutes(appManager: ApplicationManager): Router {
@ -177,7 +165,6 @@ function restoreStubs() {
} }
interface SupervisorAPIOpts { interface SupervisorAPIOpts {
config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
deviceState: DeviceState; deviceState: DeviceState;
apiBinder: APIBinder; apiBinder: APIBinder;

View File

@ -1,8 +1,11 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as db from '../../src/db'; import * as db from '../../src/db';
import * as config from '../../src/config';
export = async function () { export = async function () {
await db.initialized; await db.initialized;
await config.initialized;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const result = await trx.raw(` const result = await trx.raw(`
SELECT name, sql SELECT name, sql
@ -51,11 +54,15 @@ export = async function () {
'./test/data/config-apibinder-offline.json', './test/data/config-apibinder-offline.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline.json'), fs.readFileSync('./test/data/testconfig-apibinder-offline.json'),
); );
return fs.writeFileSync( fs.writeFileSync(
'./test/data/config-apibinder-offline2.json', './test/data/config-apibinder-offline2.json',
fs.readFileSync('./test/data/testconfig-apibinder-offline2.json'), fs.readFileSync('./test/data/testconfig-apibinder-offline2.json'),
); );
} catch (e) { } catch (e) {
/* ignore /*/ /* ignore /*/
} }
// @ts-expect-error using private properties
config.configJsonBackend.cache = await config.configJsonBackend.read();
await config.generateRequiredFields();
}; };