mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-22 10:21:01 +00:00
Merge pull request #865 from balena-io/fully-typed-config
Add types to the config module, and remove unnecessary casts and validations
This commit is contained in:
commit
7524b3a109
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "balena-supervisor",
|
"name": "balena-supervisor",
|
||||||
"version": "9.2.8",
|
"version": "9.2.10",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -64,7 +64,9 @@
|
|||||||
"event-stream": "3.3.4",
|
"event-stream": "3.3.4",
|
||||||
"express": "^4.0.0",
|
"express": "^4.0.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^0.5.2",
|
"fork-ts-checker-webpack-plugin": "^0.5.2",
|
||||||
|
"fp-ts": "^1.12.2",
|
||||||
"husky": "^1.3.0",
|
"husky": "^1.3.0",
|
||||||
|
"io-ts": "^1.5.1",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"json-mask": "^0.3.8",
|
"json-mask": "^0.3.8",
|
||||||
"knex": "~0.15.2",
|
"knex": "~0.15.2",
|
||||||
|
@ -20,11 +20,11 @@ import {
|
|||||||
} from './lib/errors';
|
} from './lib/errors';
|
||||||
import { pathExistsOnHost } from './lib/fs-utils';
|
import { pathExistsOnHost } from './lib/fs-utils';
|
||||||
import { request, requestOpts } from './lib/request';
|
import { request, requestOpts } from './lib/request';
|
||||||
import { ConfigValue } from './lib/types';
|
|
||||||
import { writeLock } from './lib/update-lock';
|
import { writeLock } from './lib/update-lock';
|
||||||
import { checkInt, checkTruthy } from './lib/validation';
|
|
||||||
import { DeviceApplicationState } from './types/state';
|
import { DeviceApplicationState } from './types/state';
|
||||||
|
|
||||||
|
import { SchemaReturn as ConfigSchemaType } from './config/schema-type';
|
||||||
|
|
||||||
const REPORT_SUCCESS_DELAY = 1000;
|
const REPORT_SUCCESS_DELAY = 1000;
|
||||||
const MAX_REPORT_RETRY_DELAY = 60000;
|
const MAX_REPORT_RETRY_DELAY = 60000;
|
||||||
|
|
||||||
@ -46,16 +46,8 @@ interface APIBinderConstructOpts {
|
|||||||
eventTracker: EventTracker;
|
eventTracker: EventTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyExchangeOpts {
|
|
||||||
uuid: ConfigValue;
|
|
||||||
deviceApiKey: ConfigValue;
|
|
||||||
apiTimeout: ConfigValue;
|
|
||||||
apiEndpoint: ConfigValue;
|
|
||||||
provisioningApiKey: ConfigValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Device {
|
interface Device {
|
||||||
id: string;
|
id: number;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@ -65,6 +57,8 @@ interface DevicePinInfo {
|
|||||||
commit: string;
|
commit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeyExchangeOpts = ConfigSchemaType<'provisioningOptions'>;
|
||||||
|
|
||||||
export class APIBinder {
|
export class APIBinder {
|
||||||
public router: express.Router;
|
public router: express.Router;
|
||||||
|
|
||||||
@ -126,8 +120,7 @@ export class APIBinder {
|
|||||||
const timeSinceLastFetch = process.hrtime(this.lastTargetStateFetch);
|
const timeSinceLastFetch = process.hrtime(this.lastTargetStateFetch);
|
||||||
const timeSinceLastFetchMs =
|
const timeSinceLastFetchMs =
|
||||||
timeSinceLastFetch[0] * 1000 + timeSinceLastFetch[1] / 1e6;
|
timeSinceLastFetch[0] * 1000 + timeSinceLastFetch[1] / 1e6;
|
||||||
const stateFetchHealthy =
|
const stateFetchHealthy = timeSinceLastFetchMs < 2 * appUpdatePollInterval;
|
||||||
timeSinceLastFetchMs < 2 * (appUpdatePollInterval as number);
|
|
||||||
const stateReportHealthy =
|
const stateReportHealthy =
|
||||||
!connectivityCheckEnabled ||
|
!connectivityCheckEnabled ||
|
||||||
!this.deviceState.connected ||
|
!this.deviceState.connected ||
|
||||||
@ -146,7 +139,7 @@ export class APIBinder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = url.resolve(apiEndpoint as string, '/v5/');
|
const baseUrl = url.resolve(apiEndpoint, '/v5/');
|
||||||
const passthrough = _.cloneDeep(requestOpts);
|
const passthrough = _.cloneDeep(requestOpts);
|
||||||
passthrough.headers =
|
passthrough.headers =
|
||||||
passthrough.headers != null ? passthrough.headers : {};
|
passthrough.headers != null ? passthrough.headers : {};
|
||||||
@ -210,16 +203,13 @@ export class APIBinder {
|
|||||||
// Either we haven't reported our initial config or we've been re-provisioned
|
// Either we haven't reported our initial config or we've been re-provisioned
|
||||||
if (apiEndpoint !== initialConfigReported) {
|
if (apiEndpoint !== initialConfigReported) {
|
||||||
console.log('Reporting initial configuration');
|
console.log('Reporting initial configuration');
|
||||||
await this.reportInitialConfig(
|
await this.reportInitialConfig(apiEndpoint, bootstrapRetryDelay);
|
||||||
apiEndpoint as string,
|
|
||||||
bootstrapRetryDelay as number,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting current state report');
|
console.log('Starting current state report');
|
||||||
await this.startCurrentStateReport();
|
await this.startCurrentStateReport();
|
||||||
|
|
||||||
await this.loadBackupFromMigration(bootstrapRetryDelay as number);
|
await this.loadBackupFromMigration(bootstrapRetryDelay);
|
||||||
|
|
||||||
this.readyForUpdates = true;
|
this.readyForUpdates = true;
|
||||||
console.log('Starting target state poll');
|
console.log('Starting target state poll');
|
||||||
@ -288,7 +278,7 @@ export class APIBinder {
|
|||||||
id,
|
id,
|
||||||
body: updatedFields,
|
body: updatedFields,
|
||||||
})
|
})
|
||||||
.timeout(conf.apiTimeout as number);
|
.timeout(conf.apiTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async provisionDependentDevice(device: Device): Promise<Device> {
|
public async provisionDependentDevice(device: Device): Promise<Device> {
|
||||||
@ -325,7 +315,7 @@ export class APIBinder {
|
|||||||
return (await this.balenaApi
|
return (await this.balenaApi
|
||||||
.post({ resource: 'device', body: device })
|
.post({ resource: 'device', body: device })
|
||||||
// TODO: Remove the `as number` when we fix the config typings
|
// TODO: Remove the `as number` when we fix the config typings
|
||||||
.timeout(conf.apiTimeout as number)) as Device;
|
.timeout(conf.apiTimeout)) as Device;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTargetState(): Promise<DeviceApplicationState> {
|
public async getTargetState(): Promise<DeviceApplicationState> {
|
||||||
@ -354,7 +344,7 @@ export class APIBinder {
|
|||||||
|
|
||||||
return await this.cachedBalenaApi
|
return await this.cachedBalenaApi
|
||||||
._request(requestParams)
|
._request(requestParams)
|
||||||
.timeout(apiTimeout as number);
|
.timeout(apiTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Once 100% typescript, change this to a native promise
|
// TODO: Once 100% typescript, change this to a native promise
|
||||||
@ -451,7 +441,7 @@ export class APIBinder {
|
|||||||
'localMode',
|
'localMode',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (checkTruthy(conf.localMode || false)) {
|
if (conf.localMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,12 +450,17 @@ export class APIBinder {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const apiEndpoint = conf.apiEndpoint;
|
||||||
|
const uuid = conf.uuid;
|
||||||
|
if (uuid == null || apiEndpoint == null) {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'No uuid or apiEndpoint provided to ApiBinder.report',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await Bluebird.resolve(
|
await Bluebird.resolve(
|
||||||
this.sendReportPatch(stateDiff, conf as {
|
this.sendReportPatch(stateDiff, { apiEndpoint, uuid }),
|
||||||
uuid: string;
|
).timeout(conf.apiTimeout);
|
||||||
apiEndpoint: string;
|
|
||||||
}),
|
|
||||||
).timeout(conf.apiTimeout as number);
|
|
||||||
|
|
||||||
this.stateReportErrors = 0;
|
this.stateReportErrors = 0;
|
||||||
_.assign(this.lastReportedState.local, stateDiff.local);
|
_.assign(this.lastReportedState.local, stateDiff.local);
|
||||||
@ -523,14 +518,7 @@ export class APIBinder {
|
|||||||
|
|
||||||
private async pollTargetState(): Promise<void> {
|
private async pollTargetState(): Promise<void> {
|
||||||
// TODO: Remove the checkInt here with the config changes
|
// TODO: Remove the checkInt here with the config changes
|
||||||
let pollInterval = checkInt((await this.config.get(
|
let pollInterval = await this.config.get('appUpdatePollInterval');
|
||||||
'appUpdatePollInterval',
|
|
||||||
)) as string);
|
|
||||||
if (!_.isNumber(pollInterval)) {
|
|
||||||
throw new InternalInconsistencyError(
|
|
||||||
'appUpdatePollInterval not a number in ApiBinder.pollTargetState',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.getAndSetTargetState(false);
|
await this.getAndSetTargetState(false);
|
||||||
@ -556,6 +544,13 @@ export class APIBinder {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceId = await this.config.get('deviceId');
|
const deviceId = await this.config.get('deviceId');
|
||||||
|
|
||||||
|
if (deviceId == null) {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'Device ID not defined in ApiBinder.pinDevice',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const release = await this.balenaApi.get({
|
const release = await this.balenaApi.get({
|
||||||
resource: 'release',
|
resource: 'release',
|
||||||
options: {
|
options: {
|
||||||
@ -577,7 +572,7 @@ export class APIBinder {
|
|||||||
|
|
||||||
await this.balenaApi.patch({
|
await this.balenaApi.patch({
|
||||||
resource: 'device',
|
resource: 'device',
|
||||||
id: deviceId as number,
|
id: deviceId,
|
||||||
body: {
|
body: {
|
||||||
should_be_running__release: releaseId,
|
should_be_running__release: releaseId,
|
||||||
},
|
},
|
||||||
@ -665,15 +660,11 @@ export class APIBinder {
|
|||||||
opts?: KeyExchangeOpts,
|
opts?: KeyExchangeOpts,
|
||||||
): Promise<Device> {
|
): Promise<Device> {
|
||||||
if (opts == null) {
|
if (opts == null) {
|
||||||
// FIXME: This casting shouldn't be necessary and stems from the
|
opts = await this.config.get('provisioningOptions');
|
||||||
// meta-option provioningOptions not returning a ConfigValue
|
|
||||||
opts = ((await this.config.get(
|
|
||||||
'provisioningOptions',
|
|
||||||
)) as any) as KeyExchangeOpts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = opts.uuid as string;
|
const uuid = opts.uuid;
|
||||||
const apiTimeout = opts.apiTimeout as number;
|
const apiTimeout = opts.apiTimeout;
|
||||||
if (!(uuid && apiTimeout)) {
|
if (!(uuid && apiTimeout)) {
|
||||||
throw new InternalInconsistencyError(
|
throw new InternalInconsistencyError(
|
||||||
'UUID and apiTimeout should be defined in exchangeKeyAndGetDevice',
|
'UUID and apiTimeout should be defined in exchangeKeyAndGetDevice',
|
||||||
@ -685,7 +676,7 @@ export class APIBinder {
|
|||||||
if (opts.deviceApiKey != null) {
|
if (opts.deviceApiKey != null) {
|
||||||
const device = await this.fetchDevice(
|
const device = await this.fetchDevice(
|
||||||
uuid,
|
uuid,
|
||||||
opts.deviceApiKey as string,
|
opts.deviceApiKey,
|
||||||
apiTimeout,
|
apiTimeout,
|
||||||
);
|
);
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
@ -695,9 +686,14 @@ export class APIBinder {
|
|||||||
|
|
||||||
// If it's not valid or doesn't exist then we try to use the
|
// If it's not valid or doesn't exist then we try to use the
|
||||||
// user/provisioning api key for the exchange
|
// user/provisioning api key for the exchange
|
||||||
|
if (!opts.provisioningApiKey) {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'Required a provisioning key in exchangeKeyAndGetDevice',
|
||||||
|
);
|
||||||
|
}
|
||||||
const device = await this.fetchDevice(
|
const device = await this.fetchDevice(
|
||||||
uuid,
|
uuid,
|
||||||
opts.provisioningApiKey as string,
|
opts.provisioningApiKey,
|
||||||
apiTimeout,
|
apiTimeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -746,10 +742,7 @@ export class APIBinder {
|
|||||||
private async provision() {
|
private async provision() {
|
||||||
let device: Device | null = null;
|
let device: Device | null = null;
|
||||||
// FIXME: Config typing
|
// FIXME: Config typing
|
||||||
const opts = ((await this.config.get(
|
const opts = await this.config.get('provisioningOptions');
|
||||||
'provisioningOptions',
|
|
||||||
)) as any) as Dictionary<any>;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
opts.registered_at != null &&
|
opts.registered_at != null &&
|
||||||
opts.deviceId != null &&
|
opts.deviceId != null &&
|
||||||
@ -762,10 +755,7 @@ export class APIBinder {
|
|||||||
console.log(
|
console.log(
|
||||||
'Device is registered but no device id available, attempting key exchange',
|
'Device is registered but no device id available, attempting key exchange',
|
||||||
);
|
);
|
||||||
device =
|
device = (await this.exchangeKeyAndGetDeviceOrRegenerate(opts)) || null;
|
||||||
(await this.exchangeKeyAndGetDeviceOrRegenerate(
|
|
||||||
opts as KeyExchangeOpts,
|
|
||||||
)) || null;
|
|
||||||
} else if (opts.registered_at == null) {
|
} else if (opts.registered_at == null) {
|
||||||
console.log('New device detected. Provisioning...');
|
console.log('New device detected. Provisioning...');
|
||||||
try {
|
try {
|
||||||
@ -774,9 +764,7 @@ export class APIBinder {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (DuplicateUuidError(err)) {
|
if (DuplicateUuidError(err)) {
|
||||||
console.log('UUID already registered, trying a key exchange');
|
console.log('UUID already registered, trying a key exchange');
|
||||||
await this.exchangeKeyAndGetDeviceOrRegenerate(
|
await this.exchangeKeyAndGetDeviceOrRegenerate(opts);
|
||||||
opts as KeyExchangeOpts,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -785,7 +773,7 @@ export class APIBinder {
|
|||||||
console.log(
|
console.log(
|
||||||
'Device is registered but we still have an apiKey, attempting key exchange',
|
'Device is registered but we still have an apiKey, attempting key exchange',
|
||||||
);
|
);
|
||||||
device = await this.exchangeKeyAndGetDevice(opts as KeyExchangeOpts);
|
device = await this.exchangeKeyAndGetDevice(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!device) {
|
if (!device) {
|
||||||
@ -811,13 +799,7 @@ export class APIBinder {
|
|||||||
this.eventTracker.track('Device bootstrap success');
|
this.eventTracker.track('Device bootstrap success');
|
||||||
|
|
||||||
// Now check if we need to pin the device
|
// Now check if we need to pin the device
|
||||||
const toPin = await this.config.get('pinDevice');
|
const pinValue = await this.config.get('pinDevice');
|
||||||
let pinValue: DevicePinInfo | null = null;
|
|
||||||
try {
|
|
||||||
pinValue = JSON.parse(toPin as string);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Warning: Malformed pinDevice value in supervisor database');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinValue != null) {
|
if (pinValue != null) {
|
||||||
if (pinValue.app == null || pinValue.commit == null) {
|
if (pinValue.app == null || pinValue.commit == null) {
|
||||||
|
@ -140,7 +140,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
@images.save(step.image)
|
@images.save(step.image)
|
||||||
cleanup: (step) =>
|
cleanup: (step) =>
|
||||||
@config.get('localMode').then (localMode) =>
|
@config.get('localMode').then (localMode) =>
|
||||||
if !checkTruthy(localMode)
|
if !localMode
|
||||||
@images.cleanup()
|
@images.cleanup()
|
||||||
createNetworkOrVolume: (step) =>
|
createNetworkOrVolume: (step) =>
|
||||||
if step.model is 'network'
|
if step.model is 'network'
|
||||||
@ -762,7 +762,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
getTargetApps: =>
|
getTargetApps: =>
|
||||||
@config.getMany(['apiEndpoint', 'localMode']). then ({ apiEndpoint, localMode }) =>
|
@config.getMany(['apiEndpoint', 'localMode']). then ({ apiEndpoint, localMode }) =>
|
||||||
source = apiEndpoint
|
source = apiEndpoint
|
||||||
if checkTruthy(localMode)
|
if localMode
|
||||||
source = 'local'
|
source = 'local'
|
||||||
Promise.map(@db.models('app').where({ source }), @normaliseAndExtendAppFromDB)
|
Promise.map(@db.models('app').where({ source }), @normaliseAndExtendAppFromDB)
|
||||||
.map (app) =>
|
.map (app) =>
|
||||||
@ -847,8 +847,6 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
return { imagesToSave, imagesToRemove }
|
return { imagesToSave, imagesToRemove }
|
||||||
|
|
||||||
_inferNextSteps: (cleanupNeeded, availableImages, downloading, supervisorNetworkReady, current, target, ignoreImages, { localMode, delta }) =>
|
_inferNextSteps: (cleanupNeeded, availableImages, downloading, supervisorNetworkReady, current, target, ignoreImages, { localMode, delta }) =>
|
||||||
localMode = checkTruthy(localMode)
|
|
||||||
delta = checkTruthy(delta)
|
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
if localMode
|
if localMode
|
||||||
ignoreImages = true
|
ignoreImages = true
|
||||||
@ -895,7 +893,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
return Promise.try(fn)
|
return Promise.try(fn)
|
||||||
@config.get('lockOverride')
|
@config.get('lockOverride')
|
||||||
.then (lockOverride) ->
|
.then (lockOverride) ->
|
||||||
return checkTruthy(lockOverride) or force
|
return lockOverride or force
|
||||||
.then (force) ->
|
.then (force) ->
|
||||||
updateLock.lock(appId, { force }, fn)
|
updateLock.lock(appId, { force }, fn)
|
||||||
|
|
||||||
@ -919,7 +917,7 @@ module.exports = class ApplicationManager extends EventEmitter
|
|||||||
|
|
||||||
getRequiredSteps: (currentState, targetState, extraState, ignoreImages = false) =>
|
getRequiredSteps: (currentState, targetState, extraState, ignoreImages = false) =>
|
||||||
{ cleanupNeeded, availableImages, downloading, supervisorNetworkReady, delta, localMode } = extraState
|
{ cleanupNeeded, availableImages, downloading, supervisorNetworkReady, delta, localMode } = extraState
|
||||||
conf = _.mapValues({ delta, localMode }, (v) -> checkTruthy(v))
|
conf = { delta, localMode }
|
||||||
if conf.localMode
|
if conf.localMode
|
||||||
cleanupNeeded = false
|
cleanupNeeded = false
|
||||||
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf)
|
@_inferNextSteps(cleanupNeeded, availableImages, downloading, supervisorNetworkReady, currentState, targetState, ignoreImages, conf)
|
||||||
|
@ -59,7 +59,7 @@ module.exports = class Images extends EventEmitter
|
|||||||
.catch =>
|
.catch =>
|
||||||
@reportChange(image.imageId, _.merge(_.clone(image), { status: 'Downloading', downloadProgress: 0 }))
|
@reportChange(image.imageId, _.merge(_.clone(image), { status: 'Downloading', downloadProgress: 0 }))
|
||||||
Promise.try =>
|
Promise.try =>
|
||||||
if validation.checkTruthy(opts.delta) and opts.deltaSource?
|
if opts.delta and opts.deltaSource?
|
||||||
@logger.logSystemEvent(logTypes.downloadImageDelta, { image })
|
@logger.logSystemEvent(logTypes.downloadImageDelta, { image })
|
||||||
@inspectByName(opts.deltaSource)
|
@inspectByName(opts.deltaSource)
|
||||||
.then (srcImage) =>
|
.then (srcImage) =>
|
||||||
|
@ -3,8 +3,8 @@ import * as _ from 'lodash';
|
|||||||
import { fs } from 'mz';
|
import { fs } from 'mz';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { ConfigSchema, ConfigValue } from '../lib/types';
|
|
||||||
import { readLock, writeLock } from '../lib/update-lock';
|
import { readLock, writeLock } from '../lib/update-lock';
|
||||||
|
import * as Schema from './schema';
|
||||||
|
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import { writeAndSyncFile, writeFileAtomic } from '../lib/fs-utils';
|
import { writeAndSyncFile, writeFileAtomic } from '../lib/fs-utils';
|
||||||
@ -15,11 +15,11 @@ export default class ConfigJsonConfigBackend {
|
|||||||
private writeLockConfigJson: () => Promise.Disposer<() => void>;
|
private writeLockConfigJson: () => Promise.Disposer<() => void>;
|
||||||
|
|
||||||
private configPath?: string;
|
private configPath?: string;
|
||||||
private cache: { [key: string]: ConfigValue } = {};
|
private cache: { [key: string]: unknown } = {};
|
||||||
|
|
||||||
private schema: ConfigSchema;
|
private schema: Schema.Schema;
|
||||||
|
|
||||||
public constructor(schema: ConfigSchema, configPath?: string) {
|
public constructor(schema: Schema.Schema, configPath?: string) {
|
||||||
this.configPath = configPath;
|
this.configPath = configPath;
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
|
|
||||||
@ -35,10 +35,12 @@ export default class ConfigJsonConfigBackend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(keyVals: { [key: string]: ConfigValue }): Promise<void> {
|
public set<T extends Schema.SchemaKey>(
|
||||||
|
keyVals: { [key in T]: unknown },
|
||||||
|
): Promise<void> {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
return Promise.using(this.writeLockConfigJson(), () => {
|
return Promise.using(this.writeLockConfigJson(), () => {
|
||||||
return Promise.mapSeries(_.keys(keyVals), (key: string) => {
|
return Promise.mapSeries(_.keys(keyVals) as T[], (key: T) => {
|
||||||
const value = keyVals[key];
|
const value = keyVals[key];
|
||||||
|
|
||||||
if (this.cache[key] !== value) {
|
if (this.cache[key] !== value) {
|
||||||
@ -62,7 +64,7 @@ export default class ConfigJsonConfigBackend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(key: string): Promise<ConfigValue> {
|
public get(key: string): Promise<unknown> {
|
||||||
return Promise.using(this.readLockConfigJson(), () => {
|
return Promise.using(this.readLockConfigJson(), () => {
|
||||||
return Promise.resolve(this.cache[key]);
|
return Promise.resolve(this.cache[key]);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { Transaction } from 'knex';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
@ -8,141 +7,101 @@ import supervisorVersion = require('../lib/supervisor-version');
|
|||||||
import Config from '.';
|
import Config from '.';
|
||||||
import * as constants from '../lib/constants';
|
import * as constants from '../lib/constants';
|
||||||
import * as osRelease from '../lib/os-release';
|
import * as osRelease from '../lib/os-release';
|
||||||
import { ConfigValue } from '../lib/types';
|
|
||||||
|
|
||||||
// A provider for schema entries with source 'func'
|
export const fnSchema = {
|
||||||
type ConfigProviderFunctionGetter = () => Bluebird<any>;
|
version: () => {
|
||||||
type ConfigProviderFunctionSetter = (
|
return Bluebird.resolve(supervisorVersion);
|
||||||
value: ConfigValue,
|
},
|
||||||
tx?: Transaction,
|
currentApiKey: (config: Config) => {
|
||||||
) => Bluebird<void>;
|
return config
|
||||||
type ConfigProviderFunctionRemover = () => Bluebird<void>;
|
.getMany(['apiKey', 'deviceApiKey'])
|
||||||
|
.then(({ apiKey, deviceApiKey }) => {
|
||||||
|
return apiKey || deviceApiKey;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
provisioned: (config: Config) => {
|
||||||
|
return config
|
||||||
|
.getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId'])
|
||||||
|
.then(requiredValues => {
|
||||||
|
return _.every(_.values(requiredValues));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
osVersion: () => {
|
||||||
|
return osRelease.getOSVersion(constants.hostOSVersionPath);
|
||||||
|
},
|
||||||
|
osVariant: () => {
|
||||||
|
return osRelease.getOSVariant(constants.hostOSVersionPath);
|
||||||
|
},
|
||||||
|
provisioningOptions: (config: Config) => {
|
||||||
|
return config
|
||||||
|
.getMany([
|
||||||
|
'uuid',
|
||||||
|
'userId',
|
||||||
|
'applicationId',
|
||||||
|
'apiKey',
|
||||||
|
'deviceApiKey',
|
||||||
|
'deviceType',
|
||||||
|
'apiEndpoint',
|
||||||
|
'apiTimeout',
|
||||||
|
'registered_at',
|
||||||
|
'deviceId',
|
||||||
|
])
|
||||||
|
.then(conf => {
|
||||||
|
return {
|
||||||
|
uuid: conf.uuid,
|
||||||
|
applicationId: conf.applicationId,
|
||||||
|
userId: conf.userId,
|
||||||
|
deviceType: conf.deviceType,
|
||||||
|
provisioningApiKey: conf.apiKey,
|
||||||
|
deviceApiKey: conf.deviceApiKey,
|
||||||
|
apiEndpoint: conf.apiEndpoint,
|
||||||
|
apiTimeout: conf.apiTimeout,
|
||||||
|
registered_at: conf.registered_at,
|
||||||
|
deviceId: conf.deviceId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
mixpanelHost: (config: Config) => {
|
||||||
|
return config.get('apiEndpoint').then(apiEndpoint => {
|
||||||
|
if (!apiEndpoint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const url = new URL(apiEndpoint);
|
||||||
|
return { host: url.host, path: '/mixpanel' };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
extendedEnvOptions: (config: Config) => {
|
||||||
|
return config.getMany([
|
||||||
|
'uuid',
|
||||||
|
'listenPort',
|
||||||
|
'name',
|
||||||
|
'apiSecret',
|
||||||
|
'deviceApiKey',
|
||||||
|
'version',
|
||||||
|
'deviceType',
|
||||||
|
'osVersion',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
fetchOptions: (config: Config) => {
|
||||||
|
return config.getMany([
|
||||||
|
'uuid',
|
||||||
|
'currentApiKey',
|
||||||
|
'apiEndpoint',
|
||||||
|
'deltaEndpoint',
|
||||||
|
'delta',
|
||||||
|
'deltaRequestTimeout',
|
||||||
|
'deltaApplyTimeout',
|
||||||
|
'deltaRetryCount',
|
||||||
|
'deltaRetryInterval',
|
||||||
|
'deltaVersion',
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
unmanaged: (config: Config) => {
|
||||||
|
return config.get('apiEndpoint').then(apiEndpoint => {
|
||||||
|
return !apiEndpoint;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
interface ConfigProviderFunction {
|
export type FnSchema = typeof fnSchema;
|
||||||
get: ConfigProviderFunctionGetter;
|
export type FnSchemaKey = keyof FnSchema;
|
||||||
set?: ConfigProviderFunctionSetter;
|
|
||||||
remove?: ConfigProviderFunctionRemover;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigProviderFunctions {
|
|
||||||
[key: string]: ConfigProviderFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProviderFunctions(
|
|
||||||
config: Config,
|
|
||||||
): ConfigProviderFunctions {
|
|
||||||
return {
|
|
||||||
version: {
|
|
||||||
get: () => {
|
|
||||||
return Bluebird.resolve(supervisorVersion);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentApiKey: {
|
|
||||||
get: () => {
|
|
||||||
return config
|
|
||||||
.getMany(['apiKey', 'deviceApiKey'])
|
|
||||||
.then(({ apiKey, deviceApiKey }) => {
|
|
||||||
return apiKey || deviceApiKey;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provisioned: {
|
|
||||||
get: () => {
|
|
||||||
return config
|
|
||||||
.getMany(['uuid', 'apiEndpoint', 'registered_at', 'deviceId'])
|
|
||||||
.then(requiredValues => {
|
|
||||||
return _.every(_.values(requiredValues), Boolean);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
osVersion: {
|
|
||||||
get: () => {
|
|
||||||
return osRelease.getOSVersion(constants.hostOSVersionPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
osVariant: {
|
|
||||||
get: () => {
|
|
||||||
return osRelease.getOSVariant(constants.hostOSVersionPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
provisioningOptions: {
|
|
||||||
get: () => {
|
|
||||||
return config
|
|
||||||
.getMany([
|
|
||||||
'uuid',
|
|
||||||
'userId',
|
|
||||||
'applicationId',
|
|
||||||
'apiKey',
|
|
||||||
'deviceApiKey',
|
|
||||||
'deviceType',
|
|
||||||
'apiEndpoint',
|
|
||||||
'apiTimeout',
|
|
||||||
'registered_at',
|
|
||||||
'deviceId',
|
|
||||||
])
|
|
||||||
.then(conf => {
|
|
||||||
return {
|
|
||||||
uuid: conf.uuid,
|
|
||||||
applicationId: conf.applicationId,
|
|
||||||
userId: conf.userId,
|
|
||||||
deviceType: conf.deviceType,
|
|
||||||
provisioningApiKey: conf.apiKey,
|
|
||||||
deviceApiKey: conf.deviceApiKey,
|
|
||||||
apiEndpoint: conf.apiEndpoint,
|
|
||||||
apiTimeout: conf.apiTimeout,
|
|
||||||
registered_at: conf.registered_at,
|
|
||||||
deviceId: conf.deviceId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mixpanelHost: {
|
|
||||||
get: () => {
|
|
||||||
return config.get('apiEndpoint').then(apiEndpoint => {
|
|
||||||
if (!apiEndpoint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const url = new URL(apiEndpoint as string);
|
|
||||||
return { host: url.host, path: '/mixpanel' };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extendedEnvOptions: {
|
|
||||||
get: () => {
|
|
||||||
return config.getMany([
|
|
||||||
'uuid',
|
|
||||||
'listenPort',
|
|
||||||
'name',
|
|
||||||
'apiSecret',
|
|
||||||
'deviceApiKey',
|
|
||||||
'version',
|
|
||||||
'deviceType',
|
|
||||||
'osVersion',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fetchOptions: {
|
|
||||||
get: () => {
|
|
||||||
return config.getMany([
|
|
||||||
'uuid',
|
|
||||||
'currentApiKey',
|
|
||||||
'apiEndpoint',
|
|
||||||
'deltaEndpoint',
|
|
||||||
'delta',
|
|
||||||
'deltaRequestTimeout',
|
|
||||||
'deltaApplyTimeout',
|
|
||||||
'deltaRetryCount',
|
|
||||||
'deltaRetryInterval',
|
|
||||||
'deltaVersion',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unmanaged: {
|
|
||||||
get: () => {
|
|
||||||
return config.get('apiEndpoint').then(apiEndpoint => {
|
|
||||||
return !apiEndpoint;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -4,93 +4,39 @@ import { Transaction } from 'knex';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { generateUniqueKey } from 'resin-register-device';
|
import { generateUniqueKey } from 'resin-register-device';
|
||||||
|
|
||||||
|
import { Either } from 'fp-ts/lib/Either';
|
||||||
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
import ConfigJsonConfigBackend from './configJson';
|
import ConfigJsonConfigBackend from './configJson';
|
||||||
|
|
||||||
import * as constants from '../lib/constants';
|
import * as FnSchema from './functions';
|
||||||
import { ConfigMap, ConfigSchema, ConfigValue } from '../lib/types';
|
import * as Schema from './schema';
|
||||||
import { ConfigProviderFunctions, createProviderFunctions } from './functions';
|
import { SchemaReturn, SchemaTypeKey, schemaTypes } from './schema-type';
|
||||||
|
|
||||||
import DB from '../db';
|
import DB from '../db';
|
||||||
|
import {
|
||||||
|
ConfigurationValidationError,
|
||||||
|
InternalInconsistencyError,
|
||||||
|
} from '../lib/errors';
|
||||||
|
|
||||||
interface ConfigOpts {
|
interface ConfigOpts {
|
||||||
db: DB;
|
db: DB;
|
||||||
configPath: string;
|
configPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigMap<T extends SchemaTypeKey> = { [key in T]: SchemaReturn<key> };
|
||||||
|
|
||||||
export class Config extends EventEmitter {
|
export class Config extends EventEmitter {
|
||||||
private db: DB;
|
private db: DB;
|
||||||
private configJsonBackend: ConfigJsonConfigBackend;
|
private configJsonBackend: ConfigJsonConfigBackend;
|
||||||
private providerFunctions: ConfigProviderFunctions;
|
|
||||||
|
|
||||||
public schema: ConfigSchema = {
|
|
||||||
apiEndpoint: { source: 'config.json', default: '' },
|
|
||||||
apiTimeout: { source: 'config.json', default: 15 * 60 * 1000 },
|
|
||||||
listenPort: { source: 'config.json', default: 48484 },
|
|
||||||
deltaEndpoint: { source: 'config.json', default: 'https://delta.resin.io' },
|
|
||||||
uuid: { source: 'config.json', mutable: true },
|
|
||||||
apiKey: { source: 'config.json', mutable: true, removeIfNull: true },
|
|
||||||
deviceApiKey: { source: 'config.json', mutable: true, default: '' },
|
|
||||||
deviceType: { source: 'config.json', default: 'unknown' },
|
|
||||||
username: { source: 'config.json' },
|
|
||||||
userId: { source: 'config.json' },
|
|
||||||
deviceId: { source: 'config.json', mutable: true },
|
|
||||||
registered_at: { source: 'config.json', mutable: true },
|
|
||||||
applicationId: { source: 'config.json' },
|
|
||||||
appUpdatePollInterval: {
|
|
||||||
source: 'config.json',
|
|
||||||
mutable: true,
|
|
||||||
default: 60000,
|
|
||||||
},
|
|
||||||
mixpanelToken: {
|
|
||||||
source: 'config.json',
|
|
||||||
default: constants.defaultMixpanelToken,
|
|
||||||
},
|
|
||||||
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
|
||||||
hostname: { source: 'config.json', mutable: true },
|
|
||||||
persistentLogging: { source: 'config.json', default: false, mutable: true },
|
|
||||||
|
|
||||||
version: { source: 'func' },
|
|
||||||
currentApiKey: { source: 'func' },
|
|
||||||
provisioned: { source: 'func' },
|
|
||||||
osVersion: { source: 'func' },
|
|
||||||
osVariant: { source: 'func' },
|
|
||||||
provisioningOptions: { source: 'func' },
|
|
||||||
mixpanelHost: { source: 'func' },
|
|
||||||
extendedEnvOptions: { source: 'func' },
|
|
||||||
fetchOptions: { source: 'func' },
|
|
||||||
unmanaged: { source: 'func' },
|
|
||||||
|
|
||||||
// NOTE: all 'db' values are stored and loaded as *strings*,
|
|
||||||
apiSecret: { source: 'db', mutable: true },
|
|
||||||
name: { source: 'db', mutable: true, default: 'local' },
|
|
||||||
initialConfigReported: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
initialConfigSaved: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
containersNormalised: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
loggingEnabled: { source: 'db', mutable: true, default: 'true' },
|
|
||||||
connectivityCheckEnabled: { source: 'db', mutable: true, default: 'true' },
|
|
||||||
delta: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
deltaRequestTimeout: { source: 'db', mutable: true, default: '30000' },
|
|
||||||
deltaApplyTimeout: { source: 'db', mutable: true, default: '' },
|
|
||||||
deltaRetryCount: { source: 'db', mutable: true, default: '30' },
|
|
||||||
deltaRetryInterval: { source: 'db', mutable: true, default: '10000' },
|
|
||||||
deltaVersion: { source: 'db', mutable: true, default: '2' },
|
|
||||||
lockOverride: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
legacyAppsPresent: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
// a JSON value, which is either null, or { app: number, commit: string }
|
|
||||||
pinDevice: { source: 'db', mutable: true, default: 'null' },
|
|
||||||
currentCommit: { source: 'db', mutable: true },
|
|
||||||
targetStateSet: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
localMode: { source: 'db', mutable: true, default: 'false' },
|
|
||||||
};
|
|
||||||
|
|
||||||
public constructor({ db, configPath }: ConfigOpts) {
|
public constructor({ db, configPath }: ConfigOpts) {
|
||||||
super();
|
super();
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.configJsonBackend = new ConfigJsonConfigBackend(
|
this.configJsonBackend = new ConfigJsonConfigBackend(
|
||||||
this.schema,
|
Schema.schema,
|
||||||
configPath,
|
configPath,
|
||||||
);
|
);
|
||||||
this.providerFunctions = createProviderFunctions(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(): Bluebird<void> {
|
public init(): Bluebird<void> {
|
||||||
@ -99,166 +45,155 @@ export class Config extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(key: string, trx?: Transaction): Bluebird<ConfigValue> {
|
public get<T extends SchemaTypeKey>(
|
||||||
|
key: T,
|
||||||
|
trx?: Transaction,
|
||||||
|
): Bluebird<SchemaReturn<T>> {
|
||||||
const db = trx || this.db.models.bind(this.db);
|
const db = trx || this.db.models.bind(this.db);
|
||||||
|
|
||||||
return Bluebird.try(() => {
|
return Bluebird.try(() => {
|
||||||
if (this.schema[key] == null) {
|
if (Schema.schema.hasOwnProperty(key)) {
|
||||||
|
const schemaKey = key as Schema.SchemaKey;
|
||||||
|
|
||||||
|
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 decoded = (defaultValue as t.Type<any>).decode(undefined);
|
||||||
|
|
||||||
|
this.checkValueDecode(decoded, key, undefined);
|
||||||
|
return decoded.value;
|
||||||
|
}
|
||||||
|
return defaultValue as SchemaReturn<T>;
|
||||||
|
}
|
||||||
|
const decoded = this.decodeSchema(schemaKey, value);
|
||||||
|
|
||||||
|
this.checkValueDecode(decoded, key, value);
|
||||||
|
|
||||||
|
return decoded.value;
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
this.checkValueDecode(decoded, key, value);
|
||||||
|
|
||||||
|
return decoded.value as SchemaReturn<T>;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
throw new Error(`Unknown config value ${key}`);
|
throw new Error(`Unknown config value ${key}`);
|
||||||
}
|
}
|
||||||
switch (this.schema[key].source) {
|
|
||||||
case 'func':
|
|
||||||
return this.providerFunctions[key].get().catch(e => {
|
|
||||||
console.error(`Error getting config value for ${key}`, e, e.stack);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
case 'config.json':
|
|
||||||
return this.configJsonBackend.get(key);
|
|
||||||
case 'db':
|
|
||||||
return db('config')
|
|
||||||
.select('value')
|
|
||||||
.where({ key })
|
|
||||||
.then(([conf]: [{ value: string }]) => {
|
|
||||||
if (conf != null) {
|
|
||||||
return conf.value;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).then(value => {
|
|
||||||
const schemaEntry = this.schema[key];
|
|
||||||
if (value == null && schemaEntry != null && schemaEntry.default != null) {
|
|
||||||
return schemaEntry.default;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMany(keys: string[], trx?: Transaction): Bluebird<ConfigMap> {
|
public getMany<T extends SchemaTypeKey>(
|
||||||
return Bluebird.map(keys, (key: string) => this.get(key, trx)).then(
|
keys: T[],
|
||||||
values => {
|
trx?: Transaction,
|
||||||
return _.zipObject(keys, values);
|
): Bluebird<{ [key in T]: SchemaReturn<key> }> {
|
||||||
},
|
return Bluebird.map(keys, (key: T) => this.get(key, trx)).then(values => {
|
||||||
);
|
return _.zipObject(keys, values);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(keyValues: ConfigMap, trx?: Transaction): Bluebird<void> {
|
public set<T extends SchemaTypeKey>(
|
||||||
return Bluebird.try(() => {
|
keyValues: ConfigMap<T>,
|
||||||
// Split the values based on which storage backend they use
|
trx?: Transaction,
|
||||||
type SplitConfigBackend = {
|
): Bluebird<void> {
|
||||||
configJsonVals: ConfigMap;
|
const setValuesInTransaction = (tx: Transaction) => {
|
||||||
dbVals: ConfigMap;
|
const configJsonVals: Dictionary<unknown> = {};
|
||||||
fnVals: ConfigMap;
|
const dbVals: Dictionary<unknown> = {};
|
||||||
};
|
|
||||||
const { configJsonVals, dbVals, fnVals }: SplitConfigBackend = _.reduce(
|
|
||||||
keyValues,
|
|
||||||
(acc: SplitConfigBackend, val, key) => {
|
|
||||||
if (this.schema[key] == null || !this.schema[key].mutable) {
|
|
||||||
throw new Error(
|
|
||||||
`Config field ${key} not found or is immutable in config.set`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.schema[key].source === 'config.json') {
|
|
||||||
acc.configJsonVals[key] = val;
|
|
||||||
} else if (this.schema[key].source === 'db') {
|
|
||||||
acc.dbVals[key] = val;
|
|
||||||
} else if (this.schema[key].source === 'func') {
|
|
||||||
acc.fnVals[key] = val;
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown config backend for key: ${key}, backend: ${
|
|
||||||
this.schema[key].source
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ configJsonVals: {}, dbVals: {}, fnVals: {} },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set these values, taking into account the knex transaction
|
_.each(keyValues, (v, k: T) => {
|
||||||
const setValuesInTransaction = (tx: Transaction): Bluebird<void> => {
|
const schemaKey = k as Schema.SchemaKey;
|
||||||
const dbKeys = _.keys(dbVals);
|
const source = Schema.schema[schemaKey].source;
|
||||||
return this.getMany(dbKeys, tx)
|
|
||||||
.then(oldValues => {
|
switch (source) {
|
||||||
return Bluebird.map(dbKeys, (key: string) => {
|
case 'config.json':
|
||||||
const value = dbVals[key];
|
configJsonVals[schemaKey] = v;
|
||||||
if (oldValues[key] !== value) {
|
break;
|
||||||
return this.db.upsertModel(
|
case 'db':
|
||||||
'config',
|
dbVals[schemaKey] = v;
|
||||||
{ key, value: (value || '').toString() },
|
break;
|
||||||
{ key },
|
default:
|
||||||
tx,
|
throw new Error(
|
||||||
);
|
`Unknown configuration source: ${source} for config key: ${k}`,
|
||||||
}
|
);
|
||||||
});
|
break;
|
||||||
})
|
}
|
||||||
.then(() => {
|
});
|
||||||
return Bluebird.map(_.toPairs(fnVals), ([key, value]) => {
|
|
||||||
const fn = this.providerFunctions[key];
|
const dbKeys = _.keys(dbVals) as T[];
|
||||||
if (fn.set == null) {
|
return this.getMany(dbKeys, tx)
|
||||||
throw new Error(
|
.then(oldValues => {
|
||||||
`Attempting to set provider function without set() method implemented - key: ${key}`,
|
return Bluebird.map(dbKeys, (key: T) => {
|
||||||
);
|
const value = dbVals[key];
|
||||||
}
|
|
||||||
return fn.set(value, tx);
|
// 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);
|
||||||
.then(() => {
|
|
||||||
if (!_.isEmpty(configJsonVals)) {
|
if (oldValues[key] !== value) {
|
||||||
return this.configJsonBackend.set(configJsonVals);
|
return this.db.upsertModel(
|
||||||
|
'config',
|
||||||
|
{ key, value: strValue },
|
||||||
|
{ key },
|
||||||
|
tx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!_.isEmpty(configJsonVals)) {
|
||||||
|
return this.configJsonBackend.set(configJsonVals as {
|
||||||
|
[key in Schema.SchemaKey]: unknown
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return Bluebird.try(() => {
|
||||||
|
// Firstly validate all of the types as they are being set
|
||||||
|
this.validateConfigMap(keyValues);
|
||||||
|
|
||||||
if (trx != null) {
|
if (trx != null) {
|
||||||
return setValuesInTransaction(trx).return();
|
return setValuesInTransaction(trx).return();
|
||||||
} else {
|
} else {
|
||||||
return this.db
|
return this.db
|
||||||
.transaction((tx: Transaction) => {
|
.transaction((tx: Transaction) => setValuesInTransaction(tx))
|
||||||
return setValuesInTransaction(tx);
|
|
||||||
})
|
|
||||||
.return();
|
.return();
|
||||||
}
|
}
|
||||||
})
|
}).then(() => {
|
||||||
.then(() => {
|
this.emit('change', keyValues);
|
||||||
return setImmediate(() => {
|
});
|
||||||
this.emit('change', keyValues);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.return();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public remove(key: string): Bluebird<void> {
|
public remove<T extends Schema.SchemaKey>(key: T): Bluebird<void> {
|
||||||
return Bluebird.try(() => {
|
return Bluebird.try(() => {
|
||||||
if (this.schema[key] == null || !this.schema[key].mutable) {
|
if (Schema.schema[key] == null || !Schema.schema[key].mutable) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Attempt to delete non-existent or immutable key ${key}`,
|
`Attempt to delete non-existent or immutable key ${key}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.schema[key].source === 'config.json') {
|
if (Schema.schema[key].source === 'config.json') {
|
||||||
return this.configJsonBackend.remove(key);
|
return this.configJsonBackend.remove(key);
|
||||||
} else if (this.schema[key].source === 'db') {
|
} else if (Schema.schema[key].source === 'db') {
|
||||||
return this.db
|
return this.db
|
||||||
.models('config')
|
.models('config')
|
||||||
.del()
|
.del()
|
||||||
.where({ key });
|
.where({ key });
|
||||||
} else if (this.schema[key].source === 'func') {
|
|
||||||
const mutFn = this.providerFunctions[key];
|
|
||||||
if (mutFn == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find provider function for config ${key}!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (mutFn.remove == null) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find removal provider function for config ${key}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return mutFn.remove();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown or unsupported config backend: ${this.schema[key].source}`,
|
`Unknown or unsupported config backend: ${Schema.schema[key].source}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -275,6 +210,69 @@ export class Config extends EventEmitter {
|
|||||||
return generateUniqueKey();
|
return generateUniqueKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>) {
|
||||||
|
// Just loop over every value, run the decode function, and
|
||||||
|
// throw if any value fails verification
|
||||||
|
_.map(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 (decoded.isLeft()) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot set value for ${key}, as value failed validation: ${
|
||||||
|
decoded.value
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private generateRequiredFields() {
|
private generateRequiredFields() {
|
||||||
return this.getMany([
|
return this.getMany([
|
||||||
'uuid',
|
'uuid',
|
||||||
@ -297,6 +295,31 @@ export class Config extends EventEmitter {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static valueToString(value: unknown) {
|
||||||
|
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, value: ${value}, type: ${typeof value}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkValueDecode(
|
||||||
|
decoded: Either<t.Errors, unknown>,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
): void {
|
||||||
|
if (decoded.isLeft()) {
|
||||||
|
throw new ConfigurationValidationError(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Config;
|
export default Config;
|
||||||
|
254
src/config/schema-type.ts
Normal file
254
src/config/schema-type.ts
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import * as t from 'io-ts';
|
||||||
|
|
||||||
|
import * as constants from '../lib/constants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NullOrUndefined,
|
||||||
|
PermissiveBoolean,
|
||||||
|
PermissiveNumber,
|
||||||
|
StringJSON,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const schemaTypes = {
|
||||||
|
apiEndpoint: {
|
||||||
|
type: t.string,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
apiTimeout: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 15 * 60 * 1000,
|
||||||
|
},
|
||||||
|
listenPort: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 48484,
|
||||||
|
},
|
||||||
|
deltaEndpoint: {
|
||||||
|
type: t.string,
|
||||||
|
default: 'https://delta.balena-cloud.com',
|
||||||
|
},
|
||||||
|
uuid: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
deviceApiKey: {
|
||||||
|
type: t.string,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
deviceType: {
|
||||||
|
type: t.string,
|
||||||
|
default: 'unknown',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
deviceId: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
registered_at: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
applicationId: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
appUpdatePollInterval: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 60000,
|
||||||
|
},
|
||||||
|
mixpanelToken: {
|
||||||
|
type: t.string,
|
||||||
|
default: constants.defaultMixpanelToken,
|
||||||
|
},
|
||||||
|
bootstrapRetryDelay: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 30000,
|
||||||
|
},
|
||||||
|
hostname: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
persistentLogging: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Database types
|
||||||
|
apiSecret: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: t.string,
|
||||||
|
default: 'local',
|
||||||
|
},
|
||||||
|
initialConfigReported: {
|
||||||
|
type: t.string,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
initialConfigSaved: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
containersNormalised: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loggingEnabled: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
connectivityCheckEnabled: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
delta: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
deltaRequestTimeout: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 30000,
|
||||||
|
},
|
||||||
|
deltaApplyTimeout: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
deltaRetryCount: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 30,
|
||||||
|
},
|
||||||
|
deltaRetryInterval: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 10000,
|
||||||
|
},
|
||||||
|
deltaVersion: {
|
||||||
|
type: PermissiveNumber,
|
||||||
|
default: 2,
|
||||||
|
},
|
||||||
|
lockOverride: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
legacyAppsPresent: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
pinDevice: {
|
||||||
|
type: new StringJSON<{ app: number; commit: string }>(
|
||||||
|
t.interface({ app: t.number, commit: t.string }),
|
||||||
|
),
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
currentCommit: {
|
||||||
|
type: t.string,
|
||||||
|
default: NullOrUndefined,
|
||||||
|
},
|
||||||
|
targetStateSet: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
localMode: {
|
||||||
|
type: PermissiveBoolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function schema types
|
||||||
|
// The type should be the value that the promise resolves
|
||||||
|
// to, not including the promise itself
|
||||||
|
// The type should be a union of every return type possible,
|
||||||
|
// and the default should be t.never always
|
||||||
|
version: {
|
||||||
|
type: t.string,
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
currentApiKey: {
|
||||||
|
type: t.string,
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
provisioned: {
|
||||||
|
type: t.boolean,
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
osVersion: {
|
||||||
|
type: t.union([t.string, NullOrUndefined]),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
osVariant: {
|
||||||
|
type: t.union([t.string, NullOrUndefined]),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
provisioningOptions: {
|
||||||
|
type: t.interface({
|
||||||
|
// These types are taken from the types of the individual
|
||||||
|
// config values they're made from
|
||||||
|
// TODO: It would be nice if we could take the type values
|
||||||
|
// from the definitions above and still have the types work
|
||||||
|
uuid: t.union([t.string, NullOrUndefined]),
|
||||||
|
applicationId: t.union([PermissiveNumber, NullOrUndefined]),
|
||||||
|
userId: t.union([PermissiveNumber, NullOrUndefined]),
|
||||||
|
deviceType: t.string,
|
||||||
|
provisioningApiKey: t.union([t.string, NullOrUndefined]),
|
||||||
|
deviceApiKey: t.string,
|
||||||
|
apiEndpoint: t.string,
|
||||||
|
apiTimeout: PermissiveNumber,
|
||||||
|
registered_at: t.union([PermissiveNumber, NullOrUndefined]),
|
||||||
|
deviceId: t.union([PermissiveNumber, NullOrUndefined]),
|
||||||
|
}),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
mixpanelHost: {
|
||||||
|
type: t.union([t.null, t.interface({ host: t.string, path: t.string })]),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
extendedEnvOptions: {
|
||||||
|
type: t.interface({
|
||||||
|
uuid: t.union([t.string, NullOrUndefined]),
|
||||||
|
listenPort: PermissiveNumber,
|
||||||
|
name: t.string,
|
||||||
|
apiSecret: t.union([t.string, NullOrUndefined]),
|
||||||
|
deviceApiKey: t.string,
|
||||||
|
version: t.string,
|
||||||
|
deviceType: t.string,
|
||||||
|
osVersion: t.union([t.string, NullOrUndefined]),
|
||||||
|
}),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: t.interface({
|
||||||
|
uuid: t.union([t.string, NullOrUndefined]),
|
||||||
|
currentApiKey: t.string,
|
||||||
|
apiEndpoint: t.string,
|
||||||
|
deltaEndpoint: t.string,
|
||||||
|
delta: PermissiveBoolean,
|
||||||
|
deltaRequestTimeout: PermissiveNumber,
|
||||||
|
deltaApplyTimeout: t.union([PermissiveNumber, NullOrUndefined]),
|
||||||
|
deltaRetryCount: PermissiveNumber,
|
||||||
|
deltaRetryInterval: PermissiveNumber,
|
||||||
|
deltaVersion: PermissiveNumber,
|
||||||
|
}),
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
unmanaged: {
|
||||||
|
type: t.boolean,
|
||||||
|
default: t.never,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SchemaType = typeof schemaTypes;
|
||||||
|
export type SchemaTypeKey = keyof SchemaType;
|
||||||
|
|
||||||
|
export type RealType<T> = T extends t.Type<any> ? t.TypeOf<T> : T;
|
||||||
|
export type SchemaReturn<T extends SchemaTypeKey> =
|
||||||
|
| t.TypeOf<SchemaType[T]['type']>
|
||||||
|
| RealType<SchemaType[T]['default']>;
|
191
src/config/schema.ts
Normal file
191
src/config/schema.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
export const schema = {
|
||||||
|
apiEndpoint: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
apiTimeout: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
listenPort: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaEndpoint: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
uuid: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: true,
|
||||||
|
},
|
||||||
|
deviceApiKey: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deviceType: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deviceId: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
registered_at: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
applicationId: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
appUpdatePollInterval: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
mixpanelToken: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
bootstrapRetryDelay: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: false,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
hostname: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
persistentLogging: {
|
||||||
|
source: 'config.json',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
apiSecret: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
initialConfigReported: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
initialConfigSaved: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
containersNormalised: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
loggingEnabled: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
connectivityCheckEnabled: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
delta: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaRequestTimeout: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaApplyTimeout: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaRetryCount: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaRetryInterval: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
deltaVersion: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
lockOverride: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
legacyAppsPresent: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
pinDevice: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
currentCommit: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
targetStateSet: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
localMode: {
|
||||||
|
source: 'db',
|
||||||
|
mutable: true,
|
||||||
|
removeIfNull: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Schema = typeof schema;
|
||||||
|
export type SchemaKey = keyof Schema;
|
114
src/config/types.ts
Normal file
114
src/config/types.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import * as t from 'io-ts';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
import { InternalInconsistencyError } from '../lib/errors';
|
||||||
|
import { checkTruthy } from '../lib/validation';
|
||||||
|
|
||||||
|
const permissiveValue = t.union([
|
||||||
|
t.boolean,
|
||||||
|
t.string,
|
||||||
|
t.number,
|
||||||
|
t.null,
|
||||||
|
t.undefined,
|
||||||
|
]);
|
||||||
|
type PermissiveType = typeof permissiveValue;
|
||||||
|
|
||||||
|
export const PermissiveBoolean = new t.Type<boolean, t.TypeOf<PermissiveType>>(
|
||||||
|
'PermissiveBoolean',
|
||||||
|
_.isBoolean,
|
||||||
|
(m, c) =>
|
||||||
|
permissiveValue.validate(m, c).chain(v => {
|
||||||
|
switch (typeof v) {
|
||||||
|
case 'boolean':
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
const val = checkTruthy(v);
|
||||||
|
if (val == null) {
|
||||||
|
return t.failure(v, c);
|
||||||
|
}
|
||||||
|
return t.success(val);
|
||||||
|
case 'undefined':
|
||||||
|
return t.success(false);
|
||||||
|
case 'object':
|
||||||
|
if (_.isNull(v)) {
|
||||||
|
return t.success(false);
|
||||||
|
} else {
|
||||||
|
return t.failure(v, c);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return t.failure(v, c);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'Encode not defined for PermissiveBoolean',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const PermissiveNumber = new t.Type<number, string | number>(
|
||||||
|
'PermissiveNumber',
|
||||||
|
_.isNumber,
|
||||||
|
(m, c) =>
|
||||||
|
t
|
||||||
|
.union([t.string, t.number])
|
||||||
|
.validate(m, c)
|
||||||
|
.chain(v => {
|
||||||
|
switch (typeof v) {
|
||||||
|
case 'number':
|
||||||
|
return t.success(v);
|
||||||
|
case 'string':
|
||||||
|
const i = parseInt(v, 10);
|
||||||
|
if (_.isNaN(i)) {
|
||||||
|
return t.failure(v, c);
|
||||||
|
}
|
||||||
|
return t.success(i);
|
||||||
|
default:
|
||||||
|
return t.failure(v, c);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'Encode not defined for PermissiveNumber',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Define this differently, so that we can add a generic to it
|
||||||
|
export class StringJSON<T> extends t.Type<T, string> {
|
||||||
|
readonly _tag: 'StringJSON' = 'StringJSON';
|
||||||
|
constructor(type: t.InterfaceType<any>) {
|
||||||
|
super(
|
||||||
|
'StringJSON',
|
||||||
|
(m): m is T => type.decode(m).isRight(),
|
||||||
|
(m, c) =>
|
||||||
|
// Accept either an object, or a string which represents the
|
||||||
|
// object
|
||||||
|
t
|
||||||
|
.union([t.string, type])
|
||||||
|
.validate(m, c)
|
||||||
|
.chain(v => {
|
||||||
|
let obj: T;
|
||||||
|
if (typeof v === 'string') {
|
||||||
|
obj = JSON.parse(v);
|
||||||
|
} else {
|
||||||
|
obj = v;
|
||||||
|
}
|
||||||
|
return type.decode(obj);
|
||||||
|
}),
|
||||||
|
() => {
|
||||||
|
throw new InternalInconsistencyError(
|
||||||
|
'Encode not defined for StringJSON',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// super(
|
||||||
|
// 'string',
|
||||||
|
// (m): m is string => typeof m === 'string',
|
||||||
|
// (m, c) => (this.is(m) ? t.success(m) : t.failure(m, c)),
|
||||||
|
// t.identity,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NullOrUndefined = t.union([t.undefined, t.null]);
|
@ -10,7 +10,6 @@ import {
|
|||||||
serviceNotFoundMessage,
|
serviceNotFoundMessage,
|
||||||
v2ServiceEndpointInputErrorMessage,
|
v2ServiceEndpointInputErrorMessage,
|
||||||
} from '../lib/messages';
|
} from '../lib/messages';
|
||||||
import { checkTruthy } from '../lib/validation';
|
|
||||||
import { doPurge, doRestart, serviceAction } from './common';
|
import { doPurge, doRestart, serviceAction } from './common';
|
||||||
|
|
||||||
import supervisorVersion = require('../lib/supervisor-version');
|
import supervisorVersion = require('../lib/supervisor-version');
|
||||||
@ -232,7 +231,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
|||||||
|
|
||||||
router.get('/v2/local/target-state', async (_req, res) => {
|
router.get('/v2/local/target-state', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const localMode = checkTruthy(await deviceState.config.get('localMode'));
|
const localMode = await deviceState.config.get('localMode');
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
@ -258,7 +257,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
|||||||
// TODO: We really should refactor the config module to provide bools
|
// TODO: We really should refactor the config module to provide bools
|
||||||
// as bools etc
|
// as bools etc
|
||||||
try {
|
try {
|
||||||
const localMode = checkTruthy(await deviceState.config.get('localMode'));
|
const localMode = await deviceState.config.get('localMode');
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Config from './config';
|
import Config from './config';
|
||||||
|
import { SchemaTypeKey } from './config/schema-type';
|
||||||
import Database, { Transaction } from './db';
|
import Database, { Transaction } from './db';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ export class DeviceConfig {
|
|||||||
private actionExecutors: DeviceActionExecutors;
|
private actionExecutors: DeviceActionExecutors;
|
||||||
private configBackend: DeviceConfigBackend | null = null;
|
private configBackend: DeviceConfigBackend | null = null;
|
||||||
|
|
||||||
private static configKeys: Dictionary<ConfigOption> = {
|
private static readonly configKeys: Dictionary<ConfigOption> = {
|
||||||
appUpdatePollInterval: {
|
appUpdatePollInterval: {
|
||||||
envVarName: 'SUPERVISOR_POLL_INTERVAL',
|
envVarName: 'SUPERVISOR_POLL_INTERVAL',
|
||||||
varType: 'int',
|
varType: 'int',
|
||||||
@ -144,7 +145,9 @@ export class DeviceConfig {
|
|||||||
if (!_.isObject(step.target)) {
|
if (!_.isObject(step.target)) {
|
||||||
throw new Error('Non-dictionary value passed to changeConfig');
|
throw new Error('Non-dictionary value passed to changeConfig');
|
||||||
}
|
}
|
||||||
await this.config.set(step.target as Dictionary<string>);
|
// 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 });
|
||||||
if (step.humanReadableTarget) {
|
if (step.humanReadableTarget) {
|
||||||
this.logger.logConfigChange(step.humanReadableTarget, {
|
this.logger.logConfigChange(step.humanReadableTarget, {
|
||||||
success: true,
|
success: true,
|
||||||
@ -200,9 +203,6 @@ export class DeviceConfig {
|
|||||||
return this.configBackend;
|
return this.configBackend;
|
||||||
}
|
}
|
||||||
const dt = await this.config.get('deviceType');
|
const dt = await this.config.get('deviceType');
|
||||||
if (!_.isString(dt)) {
|
|
||||||
throw new Error('Could not detect device type');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.configBackend = configUtils.getConfigBackend(dt) || null;
|
this.configBackend = configUtils.getConfigBackend(dt) || null;
|
||||||
|
|
||||||
@ -253,9 +253,9 @@ export class DeviceConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrent() {
|
public async getCurrent() {
|
||||||
const conf = await this.config.getMany(
|
const conf = await this.config.getMany(['deviceType'].concat(
|
||||||
['deviceType'].concat(_.keys(DeviceConfig.configKeys)),
|
_.keys(DeviceConfig.configKeys),
|
||||||
);
|
) as SchemaTypeKey[]);
|
||||||
|
|
||||||
const configBackend = await this.getConfigBackend();
|
const configBackend = await this.getConfigBackend();
|
||||||
|
|
||||||
@ -271,7 +271,7 @@ export class DeviceConfig {
|
|||||||
|
|
||||||
for (const key in DeviceConfig.configKeys) {
|
for (const key in DeviceConfig.configKeys) {
|
||||||
const { envVarName } = DeviceConfig.configKeys[key];
|
const { envVarName } = DeviceConfig.configKeys[key];
|
||||||
const confValue = conf[key];
|
const confValue = conf[key as SchemaTypeKey];
|
||||||
currentConf[envVarName] = confValue != null ? confValue.toString() : '';
|
currentConf[envVarName] = confValue != null ? confValue.toString() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -391,7 +391,7 @@ export class DeviceConfig {
|
|||||||
|
|
||||||
// Check for special case actions for the VPN
|
// Check for special case actions for the VPN
|
||||||
if (
|
if (
|
||||||
!checkTruthy(unmanaged || false) &&
|
!unmanaged &&
|
||||||
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
!_.isEmpty(target['SUPERVISOR_VPN_CONTROL']) &&
|
||||||
DeviceConfig.checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
DeviceConfig.checkBoolChanged(current, target, 'SUPERVISOR_VPN_CONTROL')
|
||||||
) {
|
) {
|
||||||
|
@ -54,7 +54,7 @@ createDeviceStateRouter = (deviceState) ->
|
|||||||
rebootOrShutdown = (req, res, action) ->
|
rebootOrShutdown = (req, res, action) ->
|
||||||
deviceState.config.get('lockOverride')
|
deviceState.config.get('lockOverride')
|
||||||
.then (lockOverride) ->
|
.then (lockOverride) ->
|
||||||
force = validation.checkTruthy(req.body.force) or validation.checkTruthy(lockOverride)
|
force = validation.checkTruthy(req.body.force) or lockOverride
|
||||||
deviceState.executeStepAction({ action }, { force })
|
deviceState.executeStepAction({ action }, { force })
|
||||||
.then (response) ->
|
.then (response) ->
|
||||||
res.status(202).json(response)
|
res.status(202).json(response)
|
||||||
@ -246,7 +246,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
init: ->
|
init: ->
|
||||||
@config.on 'change', (changedConfig) =>
|
@config.on 'change', (changedConfig) =>
|
||||||
if changedConfig.loggingEnabled?
|
if changedConfig.loggingEnabled?
|
||||||
@logger.enable(validation.checkTruthy(changedConfig.loggingEnabled))
|
@logger.enable(changedConfig.loggingEnabled)
|
||||||
if changedConfig.apiSecret?
|
if changedConfig.apiSecret?
|
||||||
@reportCurrentState(api_secret: changedConfig.apiSecret)
|
@reportCurrentState(api_secret: changedConfig.apiSecret)
|
||||||
|
|
||||||
@ -258,7 +258,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then (conf) =>
|
.then (conf) =>
|
||||||
@applications.init()
|
@applications.init()
|
||||||
.then =>
|
.then =>
|
||||||
if !validation.checkTruthy(conf.initialConfigSaved)
|
if !conf.initialConfigSaved
|
||||||
@saveInitialConfig()
|
@saveInitialConfig()
|
||||||
.then =>
|
.then =>
|
||||||
@initNetworkChecks(conf)
|
@initNetworkChecks(conf)
|
||||||
@ -280,7 +280,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then =>
|
.then =>
|
||||||
@applications.getTargetApps()
|
@applications.getTargetApps()
|
||||||
.then (targetApps) =>
|
.then (targetApps) =>
|
||||||
if !conf.provisioned or (_.isEmpty(targetApps) and !validation.checkTruthy(conf.targetStateSet))
|
if !conf.provisioned or (_.isEmpty(targetApps) and !conf.targetStateSet)
|
||||||
@loadTargetFromFile()
|
@loadTargetFromFile()
|
||||||
.finally =>
|
.finally =>
|
||||||
@config.set({ targetStateSet: 'true' })
|
@config.set({ targetStateSet: 'true' })
|
||||||
@ -296,7 +296,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@triggerApplyTarget({ initial: true })
|
@triggerApplyTarget({ initial: true })
|
||||||
|
|
||||||
initNetworkChecks: ({ apiEndpoint, connectivityCheckEnabled, unmanaged }) =>
|
initNetworkChecks: ({ apiEndpoint, connectivityCheckEnabled, unmanaged }) =>
|
||||||
return if validation.checkTruthy(unmanaged)
|
return if unmanaged
|
||||||
network.startConnectivityCheck apiEndpoint, connectivityCheckEnabled, (connected) =>
|
network.startConnectivityCheck apiEndpoint, connectivityCheckEnabled, (connected) =>
|
||||||
@connected = connected
|
@connected = connected
|
||||||
@config.on 'change', (changedConfig) ->
|
@config.on 'change', (changedConfig) ->
|
||||||
@ -499,9 +499,9 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
console.log('Device will be pinned')
|
console.log('Device will be pinned')
|
||||||
if commitToPin? and appToPin?
|
if commitToPin? and appToPin?
|
||||||
@config.set
|
@config.set
|
||||||
pinDevice: JSON.stringify {
|
pinDevice: {
|
||||||
commit: commitToPin,
|
commit: commitToPin,
|
||||||
app: appToPin,
|
app: parseInt(appToPin, 10),
|
||||||
}
|
}
|
||||||
# Ensure that this is actually a file, and not an empty path
|
# Ensure that this is actually a file, and not an empty path
|
||||||
# It can be an empty path because if the file does not exist
|
# It can be an empty path because if the file does not exist
|
||||||
|
@ -52,3 +52,11 @@ export function DuplicateUuidError(err: Error) {
|
|||||||
export class ExchangeKeyError extends TypedError {}
|
export class ExchangeKeyError extends TypedError {}
|
||||||
|
|
||||||
export class InternalInconsistencyError extends TypedError {}
|
export class InternalInconsistencyError extends TypedError {}
|
||||||
|
|
||||||
|
export class ConfigurationValidationError extends TypedError {
|
||||||
|
public constructor(key: string, value: unknown) {
|
||||||
|
super(
|
||||||
|
`There was an error validating configuration input for key: ${key}, with value: ${value}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,20 +5,3 @@ export interface EnvVarObject {
|
|||||||
export interface LabelObject {
|
export interface LabelObject {
|
||||||
[name: string]: string;
|
[name: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backwards compatibility we need to use export = Config in config.ts
|
|
||||||
// so to export these types they have been moved here
|
|
||||||
export type ConfigValue = string | number | boolean | null;
|
|
||||||
|
|
||||||
export interface ConfigMap {
|
|
||||||
[key: string]: ConfigValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConfigSchema {
|
|
||||||
[key: string]: {
|
|
||||||
source: string;
|
|
||||||
default?: any;
|
|
||||||
mutable?: boolean;
|
|
||||||
removeIfNull?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -3,8 +3,8 @@ import * as Docker from 'dockerode';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import Config from './config';
|
import Config from './config';
|
||||||
|
import { SchemaReturn, SchemaTypeKey } from './config/schema-type';
|
||||||
import Database from './db';
|
import Database from './db';
|
||||||
import { checkTruthy } from './lib/validation';
|
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,25 +25,28 @@ export class LocalModeManager {
|
|||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
// Setup a listener to catch state changes relating to local mode
|
// Setup a listener to catch state changes relating to local mode
|
||||||
this.config.on('change', changed => {
|
this.config.on(
|
||||||
if (changed.localMode != null) {
|
'change',
|
||||||
const localMode = checkTruthy(changed.localMode) || false;
|
(changed: { [key in SchemaTypeKey]: SchemaReturn<key> }) => {
|
||||||
|
if (changed.localMode != null) {
|
||||||
|
const localMode = changed.localMode || false;
|
||||||
|
|
||||||
// First switch the logger to it's correct state
|
// First switch the logger to it's correct state
|
||||||
this.logger.switchBackend(localMode);
|
this.logger.switchBackend(localMode);
|
||||||
|
|
||||||
// If we're leaving local mode, make sure to remove all of the
|
// If we're leaving local mode, make sure to remove all of the
|
||||||
// leftover artifacts
|
// leftover artifacts
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
this.removeLocalModeArtifacts();
|
this.removeLocalModeArtifacts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
// On startup, check if we're in unmanaged mode,
|
// On startup, check if we're in unmanaged mode,
|
||||||
// as local mode needs to be set
|
// as local mode needs to be set
|
||||||
let unmanagedLocalMode = false;
|
let unmanagedLocalMode = false;
|
||||||
if (checkTruthy((await this.config.get('unmanaged')) || false)) {
|
if (await this.config.get('unmanaged')) {
|
||||||
console.log('Starting up in unmanaged mode, activating local mode');
|
console.log('Starting up in unmanaged mode, activating local mode');
|
||||||
await this.config.set({ localMode: true });
|
await this.config.set({ localMode: true });
|
||||||
unmanagedLocalMode = true;
|
unmanagedLocalMode = true;
|
||||||
@ -51,8 +54,7 @@ export class LocalModeManager {
|
|||||||
|
|
||||||
const localMode =
|
const localMode =
|
||||||
// short circuit the next get if we know we're in local mode
|
// short circuit the next get if we know we're in local mode
|
||||||
unmanagedLocalMode ||
|
unmanagedLocalMode || (await this.config.get('localMode'));
|
||||||
checkTruthy((await this.config.get('localMode')) || false);
|
|
||||||
|
|
||||||
if (!localMode) {
|
if (!localMode) {
|
||||||
// Remove any leftovers if necessary
|
// Remove any leftovers if necessary
|
||||||
|
@ -4,6 +4,7 @@ import * as _ from 'lodash';
|
|||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
|
|
||||||
import Config from './config';
|
import Config from './config';
|
||||||
|
import { SchemaReturn, SchemaTypeKey } from './config/schema-type';
|
||||||
import { EventTracker } from './event-tracker';
|
import { EventTracker } from './event-tracker';
|
||||||
import blink = require('./lib/blink');
|
import blink = require('./lib/blink');
|
||||||
import * as iptables from './lib/iptables';
|
import * as iptables from './lib/iptables';
|
||||||
@ -141,24 +142,23 @@ export class SupervisorAPI {
|
|||||||
port: number,
|
port: number,
|
||||||
apiTimeout: number,
|
apiTimeout: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const localMode = (await this.config.get('localMode')) || false;
|
const localMode = await this.config.get('localMode');
|
||||||
await this.applyListeningRules(
|
await this.applyListeningRules(localMode || false, port, allowedInterfaces);
|
||||||
checkTruthy(localMode) || false,
|
|
||||||
port,
|
|
||||||
allowedInterfaces,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Monitor the switching of local mode, and change which interfaces will
|
// Monitor the switching of local mode, and change which interfaces will
|
||||||
// be listened to based on that
|
// be listened to based on that
|
||||||
this.config.on('change', (changedConfig: Dictionary<string>) => {
|
this.config.on(
|
||||||
if (changedConfig.localMode != null) {
|
'change',
|
||||||
this.applyListeningRules(
|
(changedConfig: { [key in SchemaTypeKey]: SchemaReturn<key> }) => {
|
||||||
checkTruthy(changedConfig.localMode || false) || false,
|
if (changedConfig.localMode != null) {
|
||||||
port,
|
this.applyListeningRules(
|
||||||
allowedInterfaces,
|
changedConfig.localMode || false,
|
||||||
);
|
port,
|
||||||
}
|
allowedInterfaces,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.server = this.api.listen(port);
|
this.server = this.api.listen(port);
|
||||||
this.server.timeout = apiTimeout;
|
this.server.timeout = apiTimeout;
|
||||||
|
@ -7,7 +7,6 @@ EventEmitter = require 'events'
|
|||||||
DeviceState = require './device-state'
|
DeviceState = require './device-state'
|
||||||
{ SupervisorAPI } = require './supervisor-api'
|
{ SupervisorAPI } = require './supervisor-api'
|
||||||
{ Logger } = require './logger'
|
{ Logger } = require './logger'
|
||||||
{ checkTruthy } = require './lib/validation'
|
|
||||||
|
|
||||||
constants = require './lib/constants'
|
constants = require './lib/constants'
|
||||||
|
|
||||||
@ -57,12 +56,12 @@ module.exports = class Supervisor extends EventEmitter
|
|||||||
apiEndpoint: conf.apiEndpoint,
|
apiEndpoint: conf.apiEndpoint,
|
||||||
uuid: conf.uuid,
|
uuid: conf.uuid,
|
||||||
deviceApiKey: conf.deviceApiKey,
|
deviceApiKey: conf.deviceApiKey,
|
||||||
unmanaged: checkTruthy(conf.unmanaged),
|
unmanaged: conf.unmanaged,
|
||||||
enableLogs: checkTruthy(conf.loggingEnabled),
|
enableLogs: conf.loggingEnabled,
|
||||||
localMode: checkTruthy(conf.localMode)
|
localMode: conf.localMode
|
||||||
})
|
})
|
||||||
.then =>
|
.then =>
|
||||||
if checkTruthy(conf.legacyAppsPresent)
|
if conf.legacyAppsPresent
|
||||||
console.log('Legacy app detected, running migration')
|
console.log('Legacy app detected, running migration')
|
||||||
@deviceState.normaliseLegacy(@apiBinder.balenaApi)
|
@deviceState.normaliseLegacy(@apiBinder.balenaApi)
|
||||||
.then =>
|
.then =>
|
||||||
|
@ -243,9 +243,8 @@ describe 'deviceState', ->
|
|||||||
@deviceState.applications.images.save.restore()
|
@deviceState.applications.images.save.restore()
|
||||||
@deviceState.deviceConfig.getCurrent.restore()
|
@deviceState.deviceConfig.getCurrent.restore()
|
||||||
|
|
||||||
@config.get('pinDevice').then (pinnedString) ->
|
@config.get('pinDevice').then (pinned) ->
|
||||||
pinned = JSON.parse(pinnedString)
|
expect(pinned).to.have.property('app').that.equals(1234)
|
||||||
expect(pinned).to.have.property('app').that.equals('1234')
|
|
||||||
expect(pinned).to.have.property('commit').that.equals('abcdef')
|
expect(pinned).to.have.property('commit').that.equals('abcdef')
|
||||||
|
|
||||||
it 'emits a change event when a new state is reported', ->
|
it 'emits a change event when a new state is reported', ->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user