2018-06-18 10:18:42 +00:00
|
|
|
import * as Bluebird from 'bluebird';
|
|
|
|
import { EventEmitter } from 'events';
|
|
|
|
import { Transaction } from 'knex';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
import { generateUniqueKey } from 'resin-register-device';
|
|
|
|
|
2018-11-28 14:18:33 +00:00
|
|
|
import ConfigJsonConfigBackend from './configJson';
|
2018-06-18 10:18:42 +00:00
|
|
|
|
2018-11-28 14:18:33 +00:00
|
|
|
import { ConfigProviderFunctions, createProviderFunctions } from './functions';
|
|
|
|
import * as constants from '../lib/constants';
|
|
|
|
import { ConfigMap, ConfigSchema, ConfigValue } from '../lib/types';
|
2018-06-18 10:18:42 +00:00
|
|
|
|
2018-11-28 14:18:33 +00:00
|
|
|
import DB = require('../db');
|
2018-06-18 10:18:42 +00:00
|
|
|
|
|
|
|
interface ConfigOpts {
|
|
|
|
db: DB;
|
|
|
|
configPath: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
class Config extends EventEmitter {
|
|
|
|
private db: DB;
|
|
|
|
private configJsonBackend: ConfigJsonConfigBackend;
|
2018-06-19 16:55:33 +00:00
|
|
|
private providerFunctions: ConfigProviderFunctions;
|
2018-06-18 10:18:42 +00:00
|
|
|
|
|
|
|
public schema: ConfigSchema = {
|
2018-11-26 15:37:42 +00:00
|
|
|
apiEndpoint: { source: 'config.json', default: '' },
|
2018-06-18 10:18:42 +00:00
|
|
|
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 },
|
2018-11-28 14:19:25 +00:00
|
|
|
deviceApiKey: { source: 'config.json', mutable: true, default: '' },
|
2018-11-26 16:45:16 +00:00
|
|
|
deviceType: { source: 'config.json', default: 'unknown' },
|
2018-06-18 10:18:42 +00:00
|
|
|
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' },
|
2018-11-02 14:17:58 +00:00
|
|
|
appUpdatePollInterval: {
|
|
|
|
source: 'config.json',
|
|
|
|
mutable: true,
|
|
|
|
default: 60000,
|
|
|
|
},
|
|
|
|
mixpanelToken: {
|
|
|
|
source: 'config.json',
|
|
|
|
default: constants.defaultMixpanelToken,
|
|
|
|
},
|
2018-06-18 10:18:42 +00:00
|
|
|
bootstrapRetryDelay: { source: 'config.json', default: 30000 },
|
|
|
|
hostname: { source: 'config.json', mutable: true },
|
2018-07-17 11:21:27 +00:00
|
|
|
persistentLogging: { source: 'config.json', default: false, mutable: true },
|
2018-06-18 10:18:42 +00:00
|
|
|
|
|
|
|
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' },
|
2018-12-11 14:46:03 +00:00
|
|
|
unmanaged: { source: 'func' },
|
2018-06-18 10:18:42 +00:00
|
|
|
|
|
|
|
// NOTE: all 'db' values are stored and loaded as *strings*,
|
|
|
|
apiSecret: { source: 'db', mutable: true },
|
2018-11-29 11:23:05 +00:00
|
|
|
name: { source: 'db', mutable: true, default: 'local' },
|
2018-06-18 10:18:42 +00:00
|
|
|
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' },
|
2018-06-20 11:57:16 +00:00
|
|
|
currentCommit: { source: 'db', mutable: true },
|
2018-10-18 15:36:42 +00:00
|
|
|
targetStateSet: { source: 'db', mutable: true, default: 'false' },
|
2018-12-11 13:53:10 +00:00
|
|
|
localMode: { source: 'db', mutable: true, default: 'false' },
|
2018-06-18 10:18:42 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
public constructor({ db, configPath }: ConfigOpts) {
|
|
|
|
super();
|
|
|
|
this.db = db;
|
2018-11-02 14:17:58 +00:00
|
|
|
this.configJsonBackend = new ConfigJsonConfigBackend(
|
|
|
|
this.schema,
|
|
|
|
configPath,
|
|
|
|
);
|
2018-07-10 20:12:46 +00:00
|
|
|
this.providerFunctions = createProviderFunctions(this);
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public init(): Bluebird<void> {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.configJsonBackend.init().then(() => {
|
|
|
|
return this.generateRequiredFields();
|
|
|
|
});
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public get(key: string, trx?: Transaction): Bluebird<ConfigValue> {
|
|
|
|
const db = trx || this.db.models.bind(this.db);
|
|
|
|
|
|
|
|
return Bluebird.try(() => {
|
|
|
|
if (this.schema[key] == null) {
|
|
|
|
throw new Error(`Unknown config value ${key}`);
|
|
|
|
}
|
2018-11-02 14:17:58 +00:00
|
|
|
switch (this.schema[key].source) {
|
2018-06-18 10:18:42 +00:00
|
|
|
case 'func':
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.providerFunctions[key].get().catch(e => {
|
|
|
|
console.error(`Error getting config value for ${key}`, e, e.stack);
|
|
|
|
return null;
|
|
|
|
});
|
2018-06-18 10:18:42 +00:00
|
|
|
case 'config.json':
|
|
|
|
return this.configJsonBackend.get(key);
|
|
|
|
case 'db':
|
2018-11-02 14:17:58 +00:00
|
|
|
return db('config')
|
|
|
|
.select('value')
|
|
|
|
.where({ key })
|
|
|
|
.then(([conf]: [{ value: string }]) => {
|
2018-06-18 10:18:42 +00:00
|
|
|
if (conf != null) {
|
|
|
|
return conf.value;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
});
|
|
|
|
}
|
2018-11-02 14:17:58 +00:00
|
|
|
}).then(value => {
|
|
|
|
const schemaEntry = this.schema[key];
|
|
|
|
if (value == null && schemaEntry != null && schemaEntry.default != null) {
|
|
|
|
return schemaEntry.default;
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
});
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public getMany(keys: string[], trx?: Transaction): Bluebird<ConfigMap> {
|
2018-11-02 14:17:58 +00:00
|
|
|
return Bluebird.map(keys, (key: string) => this.get(key, trx)).then(
|
|
|
|
values => {
|
2018-06-18 10:18:42 +00:00
|
|
|
return _.zipObject(keys, values);
|
2018-11-02 14:17:58 +00:00
|
|
|
},
|
|
|
|
);
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public set(keyValues: ConfigMap, trx?: Transaction): Bluebird<void> {
|
|
|
|
return Bluebird.try(() => {
|
2018-06-19 16:55:33 +00:00
|
|
|
// Split the values based on which storage backend they use
|
2018-11-02 14:17:58 +00:00
|
|
|
type SplitConfigBackend = {
|
|
|
|
configJsonVals: ConfigMap;
|
|
|
|
dbVals: ConfigMap;
|
|
|
|
fnVals: ConfigMap;
|
|
|
|
};
|
|
|
|
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: {} },
|
|
|
|
);
|
2018-06-18 10:18:42 +00:00
|
|
|
|
2018-06-19 16:55:33 +00:00
|
|
|
// Set these values, taking into account the knex transaction
|
2018-06-18 10:18:42 +00:00
|
|
|
const setValuesInTransaction = (tx: Transaction): Bluebird<void> => {
|
|
|
|
const dbKeys = _.keys(dbVals);
|
|
|
|
return this.getMany(dbKeys, tx)
|
2018-11-02 14:17:58 +00:00
|
|
|
.then(oldValues => {
|
2018-06-18 10:18:42 +00:00
|
|
|
return Bluebird.map(dbKeys, (key: string) => {
|
|
|
|
const value = dbVals[key];
|
|
|
|
if (oldValues[key] !== value) {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.db.upsertModel(
|
|
|
|
'config',
|
2018-12-14 15:05:05 +00:00
|
|
|
{ key, value: (value || '').toString() },
|
2018-11-02 14:17:58 +00:00
|
|
|
{ key },
|
|
|
|
tx,
|
|
|
|
);
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
2018-06-19 16:55:33 +00:00
|
|
|
.then(() => {
|
|
|
|
return Bluebird.map(_.toPairs(fnVals), ([key, value]) => {
|
|
|
|
const fn = this.providerFunctions[key];
|
|
|
|
if (fn.set == null) {
|
2018-11-02 14:17:58 +00:00
|
|
|
throw new Error(
|
|
|
|
`Attempting to set provider function without set() method implemented - key: ${key}`,
|
|
|
|
);
|
2018-06-19 16:55:33 +00:00
|
|
|
}
|
|
|
|
return fn.set(value, tx);
|
|
|
|
});
|
|
|
|
})
|
2018-06-18 10:18:42 +00:00
|
|
|
.then(() => {
|
|
|
|
if (!_.isEmpty(configJsonVals)) {
|
|
|
|
return this.configJsonBackend.set(configJsonVals);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
if (trx != null) {
|
|
|
|
return setValuesInTransaction(trx).return();
|
|
|
|
} else {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.db
|
|
|
|
.transaction(tx => {
|
|
|
|
return setValuesInTransaction(tx);
|
|
|
|
})
|
|
|
|
.return();
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(() => {
|
|
|
|
return setImmediate(() => {
|
|
|
|
this.emit('change', keyValues);
|
|
|
|
});
|
2018-10-15 10:12:47 +00:00
|
|
|
})
|
|
|
|
.return();
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public remove(key: string): Bluebird<void> {
|
|
|
|
return Bluebird.try(() => {
|
|
|
|
if (this.schema[key] == null || !this.schema[key].mutable) {
|
2018-11-02 14:17:58 +00:00
|
|
|
throw new Error(
|
|
|
|
`Attempt to delete non-existent or immutable key ${key}`,
|
|
|
|
);
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
if (this.schema[key].source === 'config.json') {
|
|
|
|
return this.configJsonBackend.remove(key);
|
|
|
|
} else if (this.schema[key].source === 'db') {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.db
|
|
|
|
.models('config')
|
|
|
|
.del()
|
|
|
|
.where({ key });
|
2018-06-19 16:55:33 +00:00
|
|
|
} else if (this.schema[key].source === 'func') {
|
|
|
|
const mutFn = this.providerFunctions[key];
|
|
|
|
if (mutFn == null) {
|
2018-11-02 14:17:58 +00:00
|
|
|
throw new Error(
|
|
|
|
`Could not find provider function for config ${key}!`,
|
|
|
|
);
|
2018-06-19 16:55:33 +00:00
|
|
|
}
|
|
|
|
if (mutFn.remove == null) {
|
2018-11-02 14:17:58 +00:00
|
|
|
throw new Error(
|
|
|
|
`Could not find removal provider function for config ${key}`,
|
|
|
|
);
|
2018-06-19 16:55:33 +00:00
|
|
|
}
|
|
|
|
return mutFn.remove();
|
2018-06-18 10:18:42 +00:00
|
|
|
} else {
|
2018-11-02 14:17:58 +00:00
|
|
|
throw new Error(
|
|
|
|
`Unknown or unsupported config backend: ${this.schema[key].source}`,
|
|
|
|
);
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public regenerateRegistrationFields(): Bluebird<void> {
|
|
|
|
return this.set({
|
|
|
|
uuid: this.newUniqueKey(),
|
|
|
|
deviceApiKey: this.newUniqueKey(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private newUniqueKey(): string {
|
|
|
|
return generateUniqueKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
private generateRequiredFields() {
|
|
|
|
return this.getMany([
|
|
|
|
'uuid',
|
|
|
|
'deviceApiKey',
|
|
|
|
'apiSecret',
|
2018-12-13 14:14:15 +00:00
|
|
|
'unmanaged',
|
|
|
|
]).then(({ uuid, deviceApiKey, apiSecret, unmanaged }) => {
|
2018-11-02 14:17:58 +00:00
|
|
|
// These fields need to be set regardless
|
|
|
|
if (uuid == null || apiSecret == null) {
|
|
|
|
uuid = uuid || this.newUniqueKey();
|
|
|
|
apiSecret = apiSecret || this.newUniqueKey();
|
|
|
|
}
|
|
|
|
return this.set({ uuid, apiSecret }).then(() => {
|
2018-12-13 14:14:15 +00:00
|
|
|
if (unmanaged) {
|
2018-11-02 14:17:58 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-11-28 14:19:25 +00:00
|
|
|
if (!deviceApiKey) {
|
2018-11-02 14:17:58 +00:00
|
|
|
return this.set({ deviceApiKey: this.newUniqueKey() });
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
});
|
2018-11-02 14:17:58 +00:00
|
|
|
});
|
2018-06-18 10:18:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export = Config;
|