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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import * as _ from 'lodash';
import { fs } from 'mz';
import StrictEventEmitter from 'strict-event-emitter-types';
import Config from '../config';
import * as config from '../config';
import Docker from '../lib/docker-utils';
import Logger from '../logger';
@ -28,7 +28,6 @@ import log from '../lib/supervisor-console';
interface ServiceConstructOpts {
docker: Docker;
logger: Logger;
config: Config;
}
interface ServiceManagerEvents {
@ -47,7 +46,6 @@ interface KillOpts {
export class ServiceManager extends (EventEmitter as new () => ServiceManagerEventEmitter) {
private docker: Docker;
private logger: Logger;
private config: Config;
// Whether a container has died, indexed by ID
private containerHasDied: Dictionary<boolean> = {};
@ -60,7 +58,6 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
super();
this.docker = opts.docker;
this.logger = opts.logger;
this.config = opts.config;
}
public async getAll(
@ -239,7 +236,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
}
public async create(service: Service) {
const mockContainerId = this.config.newUniqueKey();
const mockContainerId = config.newUniqueKey();
try {
const existing = await this.get(service);
if (existing.containerId == null) {
@ -257,7 +254,7 @@ export class ServiceManager extends (EventEmitter as new () => ServiceManagerEve
throw e;
}
const deviceName = await this.config.get('name');
const deviceName = await config.get('name');
if (!isValidDeviceName(deviceName)) {
throw new Error(
'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$/)
) {
// 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(
`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 Config from '.';
import * as config from '.';
import * as constants from '../lib/constants';
import * as osRelease from '../lib/os-release';
import log from '../lib/supervisor-console';
@ -14,14 +14,14 @@ export const fnSchema = {
version: () => {
return Bluebird.resolve(supervisorVersion);
},
currentApiKey: (config: Config) => {
currentApiKey: () => {
return config
.getMany(['apiKey', 'deviceApiKey'])
.then(({ apiKey, deviceApiKey }) => {
return apiKey || deviceApiKey;
});
},
provisioned: (config: Config) => {
provisioned: () => {
return config
.getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId'])
.then((requiredValues) => {
@ -50,7 +50,7 @@ export const fnSchema = {
return 'unknown';
}
},
provisioningOptions: (config: Config) => {
provisioningOptions: () => {
return config
.getMany([
'uuid',
@ -79,7 +79,7 @@ export const fnSchema = {
};
});
},
mixpanelHost: (config: Config) => {
mixpanelHost: () => {
return config.get('apiEndpoint').then((apiEndpoint) => {
if (!apiEndpoint) {
return null;
@ -88,7 +88,7 @@ export const fnSchema = {
return { host: url.host, path: '/mixpanel' };
});
},
extendedEnvOptions: (config: Config) => {
extendedEnvOptions: () => {
return config.getMany([
'uuid',
'listenPort',
@ -102,7 +102,7 @@ export const fnSchema = {
'osVersion',
]);
},
fetchOptions: (config: Config) => {
fetchOptions: () => {
return config.getMany([
'uuid',
'currentApiKey',
@ -116,7 +116,7 @@ export const fnSchema = {
'deltaVersion',
]);
},
unmanaged: (config: Config) => {
unmanaged: () => {
return config.get('apiEndpoint').then((apiEndpoint) => {
return !apiEndpoint;
});

View File

@ -1,4 +1,3 @@
import * as Bluebird from 'bluebird';
import { EventEmitter } from 'events';
import { Transaction } from 'knex';
import * as _ from 'lodash';
@ -21,10 +20,6 @@ import {
InternalInconsistencyError,
} from '../lib/errors';
interface ConfigOpts {
configPath?: string;
}
export type ConfigMap<T extends SchemaTypeKey> = {
[key in T]: SchemaReturn<key>;
};
@ -36,181 +31,235 @@ export type ConfigChangeMap<T extends SchemaTypeKey> = {
export type ConfigKey = SchemaTypeKey;
export type ConfigType<T extends ConfigKey> = SchemaReturn<T>;
interface ConfigEvents {
interface ConfigEventTypes {
change: ConfigChangeMap<SchemaTypeKey>;
}
type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEvents>;
export const configJsonBackend: ConfigJsonConfigBackend = new ConfigJsonConfigBackend(
Schema.schema,
);
export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
private configJsonBackend: ConfigJsonConfigBackend;
type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEventTypes>;
class ConfigEvents extends (EventEmitter as new () => ConfigEventEmitter) {}
const events = new ConfigEvents();
public constructor({ configPath }: ConfigOpts = {}) {
super();
this.configJsonBackend = new ConfigJsonConfigBackend(
Schema.schema,
configPath,
);
// Expose methods which make this module act as an EventEmitter
export const on: typeof events['on'] = events.on.bind(events);
export const once: typeof events['once'] = events.once.bind(events);
export const removeListener: typeof events['removeListener'] = events.removeListener.bind(
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() {
await this.generateRequiredFields();
}
export async function getMany<T extends SchemaTypeKey>(
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>(
key: T,
trx?: Transaction,
): Bluebird<SchemaReturn<T>> {
const $db = trx || db.models.bind(db);
export async function set<T extends SchemaTypeKey>(
keyValues: ConfigMap<T>,
trx?: Transaction,
): Promise<void> {
const setValuesInTransaction = async (tx: Transaction) => {
const configJsonVals: Dictionary<unknown> = {};
const dbVals: Dictionary<unknown> = {};
return Bluebird.try(() => {
if (Schema.schema.hasOwnProperty(key)) {
const schemaKey = key as Schema.SchemaKey;
_.each(keyValues, (v, k: T) => {
const schemaKey = k as Schema.SchemaKey;
const source = Schema.schema[schemaKey].source;
return this.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 (
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}`);
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}`,
);
}
});
}
public getMany<T extends SchemaTypeKey>(
keys: T[],
trx?: Transaction,
): Bluebird<{ [key in T]: SchemaReturn<key> }> {
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 dbKeys = _.keys(dbVals) as T[];
const oldValues = await getMany(dbKeys, tx);
await Promise.all(
dbKeys.map(async (key: T) => {
const value = dbVals[key];
// if we have anything other than a string, it must be converted to
// a string before being stored in the db
const strValue = Config.valueToString(value, key);
const strValue = valueToString(value, key);
if (oldValues[key] !== value) {
await db.upsertModel('config', { key, value: strValue }, { key }, tx);
}
});
}),
);
if (!_.isEmpty(configJsonVals)) {
await this.configJsonBackend.set(
configJsonVals as {
[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}`,
if (!_.isEmpty(configJsonVals)) {
await configJsonBackend.set(
configJsonVals as {
[name in Schema.SchemaKey]: unknown;
},
);
}
};
// 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> {
await this.set({
uuid: this.newUniqueKey(),
deviceApiKey: this.newUniqueKey(),
});
return isRight(type.decode(value));
}
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 generateUniqueKey();
}
return value;
}
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,
// use this in the validation of the value
const schemaTypesEntry = schemaTypes[key as SchemaTypeKey];
@ -221,122 +270,66 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
type = schemaTypesEntry.type;
}
return isRight(type.decode(value));
}
private async getSchema<T extends Schema.SchemaKey>(
key: T,
$db: Transaction,
): 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;
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>;
}
return value;
}
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 }) => {
export async function generateRequiredFields() {
return getMany(['uuid', 'deviceApiKey', 'apiSecret', 'unmanaged']).then(
({ uuid, deviceApiKey, apiSecret, unmanaged }) => {
// These fields need to be set regardless
if (uuid == null || apiSecret == null) {
uuid = uuid || this.newUniqueKey();
apiSecret = apiSecret || this.newUniqueKey();
uuid = uuid || newUniqueKey();
apiSecret = apiSecret || newUniqueKey();
}
return this.set({ uuid, apiSecret }).then(() => {
return set({ uuid, apiSecret }).then(() => {
if (unmanaged) {
return;
}
if (!deviceApiKey) {
return this.set({ deviceApiKey: this.newUniqueKey() });
return set({ deviceApiKey: newUniqueKey() });
}
});
});
}
},
);
}
private static valueToString(value: unknown, name: string) {
switch (typeof value) {
case 'object':
return JSON.stringify(value);
case 'number':
case 'string':
case 'boolean':
return value.toString();
default:
throw new InternalInconsistencyError(
`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;
function valueToString(value: unknown, name: string) {
switch (typeof value) {
case 'object':
return JSON.stringify(value);
case 'number':
case 'string':
case 'boolean':
return value.toString();
default:
throw new InternalInconsistencyError(
`Could not convert configuration value to string for storage, name: ${name}, value: ${value}, type: ${typeof value}`,
);
}
}
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 { Service } from '../compose/service';
import Volume from '../compose/volume';
import * as config from '../config';
import * as db from '../db';
import { spawnJournalctl } from '../lib/journald';
import {
@ -272,7 +273,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
router.post('/v2/local/target-state', async (req, res) => {
// let's first ensure that we're in local mode, otherwise
// this function should not do anything
const localMode = await deviceState.config.get('localMode');
const localMode = await config.get('localMode');
if (!localMode) {
return res.status(400).json({
status: 'failed',
@ -300,7 +301,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
router.get('/v2/local/device-info', async (_req, res) => {
try {
const { deviceType, deviceArch } = await applications.config.getMany([
const { deviceType, deviceArch } = await config.getMany([
'deviceType',
'deviceArch',
]);
@ -386,7 +387,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
});
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 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) => {
const deviceName = await applications.config.get('name');
const deviceName = await config.get('name');
res.json({
status: 'success',
deviceName,
@ -460,10 +461,10 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
});
router.get('/v2/device/vpn', async (_req, res) => {
const config = await deviceState.deviceConfig.getCurrent();
const conf = await deviceState.deviceConfig.getCurrent();
// Build VPNInfo
const info = {
enabled: config.SUPERVISOR_VPN_CONTROL === 'true',
enabled: conf.SUPERVISOR_VPN_CONTROL === 'true',
connected: await isVPNActive(),
};
// Return payload

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ import * as url from 'url';
import { log } from './lib/supervisor-console';
import * as db from './db';
import * as config from './config';
const mkdirpAsync = Promise.promisify(mkdirp);
@ -201,7 +202,7 @@ const createProxyvisorRouter = function (proxyvisor) {
commit,
releaseId,
environment,
config,
config: conf,
} = req.body;
const validateDeviceFields = function () {
if (isDefined(is_online) && !_.isBoolean(is_online)) {
@ -219,7 +220,7 @@ const createProxyvisorRouter = function (proxyvisor) {
if (!validObjectOrUndefined(environment)) {
return 'environment must be an object';
}
if (!validObjectOrUndefined(config)) {
if (!validObjectOrUndefined(conf)) {
return 'config must be an object';
}
return null;
@ -233,12 +234,12 @@ const createProxyvisorRouter = function (proxyvisor) {
if (isDefined(environment)) {
environment = JSON.stringify(environment);
}
if (isDefined(config)) {
config = JSON.stringify(config);
if (isDefined(conf)) {
conf = JSON.stringify(conf);
}
const fieldsToUpdateOnDB = _.pickBy(
{ status, is_online, commit, releaseId, config, environment },
{ status, is_online, commit, releaseId, config: conf, environment },
isDefined,
);
/** @type {Dictionary<any>} */
@ -343,7 +344,7 @@ const createProxyvisorRouter = function (proxyvisor) {
};
export class Proxyvisor {
constructor({ config, logger, docker, images, applications }) {
constructor({ logger, docker, images, applications }) {
this.bindToAPI = this.bindToAPI.bind(this);
this.executeStepAction = this.executeStepAction.bind(this);
this.getCurrentStates = this.getCurrentStates.bind(this);
@ -359,7 +360,6 @@ export class Proxyvisor {
this.sendUpdate = this.sendUpdate.bind(this);
this.sendDeleteHook = this.sendDeleteHook.bind(this);
this.sendUpdates = this.sendUpdates.bind(this);
this.config = config;
this.logger = logger;
this.docker = docker;
this.images = images;
@ -369,7 +369,7 @@ export class Proxyvisor {
this.router = createProxyvisorRouter(this);
this.actionExecutors = {
updateDependentTargets: (step) => {
return this.config
return config
.getMany(['currentApiKey', 'apiTimeout'])
.then(({ currentApiKey, apiTimeout }) => {
// - take each of the step.devices and update dependentDevice with it (targetCommit, targetEnvironment, targetConfig)
@ -446,7 +446,7 @@ export class Proxyvisor {
sendDependentHooks: (step) => {
return Promise.join(
this.config.get('apiTimeout'),
config.get('apiTimeout'),
this.getHookEndpoint(step.appId),
(apiTimeout, endpoint) => {
return Promise.mapSeries(step.devices, (device) => {
@ -965,7 +965,7 @@ export class Proxyvisor {
sendUpdates({ uuid }) {
return Promise.join(
db.models('dependentDevice').where({ uuid }).select(),
this.config.get('apiTimeout'),
config.get('apiTimeout'),
([dev], apiTimeout) => {
if (dev == null) {
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 morgan from 'morgan';
import Config from './config';
import * as config from './config';
import { EventTracker } from './event-tracker';
import blink = require('./lib/blink');
import * as iptables from './lib/iptables';
@ -29,7 +29,7 @@ function getKeyFromReq(req: express.Request): string | undefined {
return match?.[1];
}
function authenticate(config: Config): express.RequestHandler {
function authenticate(): express.RequestHandler {
return async (req, res, next) => {
try {
const conf = await config.getMany([
@ -76,7 +76,6 @@ const expressLogger = morgan(
);
interface SupervisorAPIConstructOpts {
config: Config;
eventTracker: EventTracker;
routers: express.Router[];
healthchecks: Array<() => Promise<boolean>>;
@ -87,7 +86,6 @@ interface SupervisorAPIStopOpts {
}
export class SupervisorAPI {
private config: Config;
private eventTracker: EventTracker;
private routers: express.Router[];
private healthchecks: Array<() => Promise<boolean>>;
@ -104,12 +102,10 @@ export class SupervisorAPI {
: this.applyListeningRules.bind(this);
public constructor({
config,
eventTracker,
routers,
healthchecks,
}: SupervisorAPIConstructOpts) {
this.config = config;
this.eventTracker = eventTracker;
this.routers = routers;
this.healthchecks = healthchecks;
@ -133,7 +129,7 @@ export class SupervisorAPI {
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.eventTracker.track('Device blink');
@ -145,8 +141,8 @@ export class SupervisorAPI {
// Expires the supervisor's API key and generates a new one.
// It also communicates the new key to the balena API.
this.api.post('/v1/regenerate-api-key', async (_req, res) => {
const secret = await this.config.newUniqueKey();
await this.config.set({ apiSecret: secret });
const secret = config.newUniqueKey();
await config.set({ apiSecret: secret });
res.status(200).send(secret);
});
@ -190,11 +186,11 @@ export class SupervisorAPI {
port: number,
apiTimeout: number,
): Promise<void> {
const localMode = await this.config.get('localMode');
const localMode = await config.get('localMode');
await this.applyRules(localMode || false, port, allowedInterfaces);
// Monitor the switching of local mode, and change which interfaces will
// be listened to based on that
this.config.on('change', (changedConfig) => {
config.on('change', (changedConfig) => {
if (changedConfig.localMode != null) {
this.applyRules(
changedConfig.localMode || false,

View File

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

View File

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

View File

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

View File

@ -3,37 +3,27 @@ import { fs } from 'mz';
import chai = require('./lib/chai-config');
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
chai.use(require('chai-events'));
const { expect } = chai;
import Config from '../src/config';
import constants = require('../src/lib/constants');
describe('Config', () => {
let conf: Config;
before(async () => {
await prepare();
conf = new Config();
await conf.init();
await conf.initialized;
});
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',
);
});
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 () => {
const id = await conf.get('applicationId');
return expect(id).to.equal(78373);
@ -107,13 +97,20 @@ describe('Config', () => {
expect(conf.get('unknownInvalidValue' as any)).to.be.rejected;
});
it('emits a change event when values are set', (done) => {
conf.on('change', (val) => {
expect(val).to.deep.equal({ name: 'someValue' });
return done();
});
it('emits a change event when values', (done) => {
const listener = (val: conf.ConfigChangeMap<SchemaTypeKey>) => {
try {
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' });
(expect(conf).to as any).emit('change');
});
it("returns an undefined OS variant if it doesn't exist", async () => {
@ -126,12 +123,6 @@ describe('Config', () => {
});
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', () => {
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 prepare = require('./lib/prepare');
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 DeviceState from '../src/device-state';
import { loadTargetFromFile } from '../src/device-state/preload';
@ -207,7 +207,6 @@ const testTargetInvalid = {
};
describe('deviceState', () => {
const config = new Config();
const logger = {
clearOutOfDateDBLogs() {
/* noop */
@ -231,7 +230,6 @@ describe('deviceState', () => {
});
deviceState = new DeviceState({
config,
eventTracker: eventTracker as any,
logger: logger as any,
apiBinder: null as any,
@ -248,7 +246,6 @@ describe('deviceState', () => {
});
(deviceState as any).deviceConfig.configBackend = new RPiConfigBackend();
await config.init();
});
after(() => {
@ -391,7 +388,7 @@ describe('deviceState', () => {
beforeEach(() => {
// This configStub will be modified in each test case so we can
// create the exact conditions we want to for testing healthchecks
configStub = stub(Config.prototype, 'get');
configStub = stub(config, 'get');
infoLobSpy = spy(Log, 'info');
});

View File

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

View File

@ -3,6 +3,7 @@ import { stripIndent } from 'common-tags';
import { child_process, fs } from 'mz';
import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import * as config from '../src/config';
import { ExtlinuxConfigBackend, RPiConfigBackend } from '../src/config/backend';
import { DeviceConfig } from '../src/device-config';
import * as fsUtils from '../src/lib/fs-utils';
@ -32,7 +33,6 @@ describe('DeviceConfig', function () {
};
return (this.deviceConfig = new DeviceConfig({
logger: this.fakeLogger,
config: this.fakeConfig,
}));
});
@ -397,19 +397,16 @@ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\
describe('ConfigFS', function () {
before(function () {
const fakeConfig = {
get(key: string) {
return Promise.try(function () {
if (key === 'deviceType') {
return 'up-board';
}
throw new Error('Unknown fake config key');
});
},
};
stub(config, 'get').callsFake((key) => {
return Promise.try(function () {
if (key === 'deviceType') {
return 'up-board';
}
throw new Error('Unknown fake config key');
});
});
this.upboardConfig = new DeviceConfig({
logger: this.fakeLogger,
config: fakeConfig as any,
});
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 () {
expect(child_process.exec).to.be.calledWith('modprobe acpi_configfs');
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');
});
});
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

View File

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

View File

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

View File

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

View File

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