mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 23:42:29 +00:00
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:
parent
10a5fb6aaf
commit
ff4a31a0e6
@ -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) {
|
||||
|
4
src/application-manager.d.ts
vendored
4
src/application-manager.d.ts
vendored
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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`,
|
||||
);
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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();
|
||||
})();
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
) {
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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}`);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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',
|
||||
]);
|
||||
|
@ -28,6 +28,10 @@ try {
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
fs.writeFileSync(
|
||||
'./test/data/config.json',
|
||||
fs.readFileSync('./test/data/testconfig.json'),
|
||||
);
|
||||
|
||||
stub(dbus, 'getBus').returns({
|
||||
getInterface: (
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user