Make the db module a singleton

We were treating the database class as a singleton, but still having to pass
around the db instance. Now we can simply require the db module and have
access to the database handle.

Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2020-05-28 18:15:33 +01:00
parent 0dc0fc77b6
commit 1d7381327e
32 changed files with 274 additions and 352 deletions

View File

@ -12,7 +12,8 @@
"build": "npm run release && webpack", "build": "npm run release && webpack",
"build:debug": "npm run release && npm run packagejson:copy", "build:debug": "npm run release && npm run packagejson:copy",
"lint": "balena-lint -e ts -e js --typescript src/ test/ typings/ build-utils/ && tsc --noEmit && tsc --noEmit --project tsconfig.js.json", "lint": "balena-lint -e ts -e js --typescript src/ test/ typings/ build-utils/ && tsc --noEmit && tsc --noEmit --project tsconfig.js.json",
"test": "npm run lint && npm run test-nolint", "test": "npm run test-nolint",
"posttest": "npm run lint",
"test-nolint": "npm run test:build && TEST=1 mocha", "test-nolint": "npm run test:build && TEST=1 mocha",
"test:build": "npm run test-build && npm run testitems:copy && npm run packagejson:copy", "test:build": "npm run test-build && npm run testitems:copy && npm run packagejson:copy",
"test:fast": "TEST=1 mocha --opts test/fast-mocha.opts", "test:fast": "TEST=1 mocha --opts test/fast-mocha.opts",

View File

@ -9,7 +9,6 @@ import * as url from 'url';
import * as deviceRegister from './lib/register-device'; import * as deviceRegister from './lib/register-device';
import Config, { ConfigType } from './config'; import Config, { ConfigType } from './config';
import Database from './db';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import { loadBackupFromMigration } from './lib/migration'; import { loadBackupFromMigration } from './lib/migration';
@ -17,9 +16,9 @@ import constants = require('./lib/constants');
import { import {
ContractValidationError, ContractValidationError,
ContractViolationError, ContractViolationError,
isHttpConflictError,
ExchangeKeyError, ExchangeKeyError,
InternalInconsistencyError, InternalInconsistencyError,
isHttpConflictError,
} from './lib/errors'; } from './lib/errors';
import * as request from './lib/request'; import * as request from './lib/request';
import { writeLock } from './lib/update-lock'; import { writeLock } from './lib/update-lock';
@ -42,8 +41,6 @@ const INTERNAL_STATE_KEYS = [
export interface APIBinderConstructOpts { export interface APIBinderConstructOpts {
config: Config; config: Config;
// FIXME: Remove this
db: Database;
eventTracker: EventTracker; eventTracker: EventTracker;
logger: Logger; logger: Logger;
} }

View File

@ -10,7 +10,6 @@ import { DeviceStatus, InstancedAppState } from './types/state';
import ImageManager, { Image } from './compose/images'; import ImageManager, { Image } from './compose/images';
import ServiceManager from './compose/service-manager'; import ServiceManager from './compose/service-manager';
import DB from './db';
import DeviceState from './device-state'; import DeviceState from './device-state';
import { APIBinder } from './api-binder'; import { APIBinder } from './api-binder';
@ -59,7 +58,6 @@ class ApplicationManager extends EventEmitter {
public volumes: VolumeManager; public volumes: VolumeManager;
public networks: NetworkManager; public networks: NetworkManager;
public config: Config; public config: Config;
public db: DB;
public images: ImageManager; public images: ImageManager;
public proxyvisor: any; public proxyvisor: any;
@ -73,7 +71,6 @@ class ApplicationManager extends EventEmitter {
public constructor({ public constructor({
logger: Logger, logger: Logger,
config: Config, config: Config,
db: DB,
eventTracker: EventTracker, eventTracker: EventTracker,
deviceState: DeviceState, deviceState: DeviceState,
apiBinder: APIBinder, apiBinder: APIBinder,
@ -96,7 +93,7 @@ class ApplicationManager extends EventEmitter {
dependent: any, dependent: any,
source: string, source: string,
transaction: Knex.Transaction, transaction: Knex.Transaction,
): Bluebird<void>; ): Promise<void>;
public getStatus(): Promise<{ public getStatus(): Promise<{
local: DeviceStatus.local.apps; local: DeviceStatus.local.apps;

View File

@ -38,6 +38,8 @@ import { createV1Api } from './device-api/v1';
import { createV2Api } from './device-api/v2'; import { createV2Api } from './device-api/v2';
import { serviceAction } from './device-api/common'; import { serviceAction } from './device-api/common';
import * as db from './db';
/** @type {Function} */ /** @type {Function} */
const readFileAsync = Promise.promisify(fs.readFile); const readFileAsync = Promise.promisify(fs.readFile);
@ -75,7 +77,7 @@ const createApplicationManagerRouter = function (applications) {
}; };
export class ApplicationManager extends EventEmitter { export class ApplicationManager extends EventEmitter {
constructor({ logger, config, db, eventTracker, deviceState, apiBinder }) { constructor({ logger, config, eventTracker, deviceState, apiBinder }) {
super(); super();
this.serviceAction = serviceAction; this.serviceAction = serviceAction;
@ -167,7 +169,6 @@ export class ApplicationManager extends EventEmitter {
this.reportOptionalContainers = this.reportOptionalContainers.bind(this); this.reportOptionalContainers = this.reportOptionalContainers.bind(this);
this.logger = logger; this.logger = logger;
this.config = config; this.config = config;
this.db = db;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.deviceState = deviceState; this.deviceState = deviceState;
this.apiBinder = apiBinder; this.apiBinder = apiBinder;
@ -175,7 +176,6 @@ export class ApplicationManager extends EventEmitter {
this.images = new Images({ this.images = new Images({
docker: this.docker, docker: this.docker,
logger: this.logger, logger: this.logger,
db: this.db,
config: this.config, config: this.config,
}); });
this.services = new ServiceManager({ this.services = new ServiceManager({
@ -194,7 +194,6 @@ export class ApplicationManager extends EventEmitter {
this.proxyvisor = new Proxyvisor({ this.proxyvisor = new Proxyvisor({
config: this.config, config: this.config,
logger: this.logger, logger: this.logger,
db: this.db,
docker: this.docker, docker: this.docker,
images: this.images, images: this.images,
applications: this, applications: this,
@ -203,18 +202,13 @@ export class ApplicationManager extends EventEmitter {
this.config, this.config,
this.docker, this.docker,
this.logger, this.logger,
this.db,
); );
this.timeSpentFetching = 0; this.timeSpentFetching = 0;
this.fetchesInProgress = 0; this.fetchesInProgress = 0;
this._targetVolatilePerImageId = {}; this._targetVolatilePerImageId = {};
this._containerStarted = {}; this._containerStarted = {};
this.targetStateWrapper = new TargetStateAccessor( this.targetStateWrapper = new TargetStateAccessor(this, this.config);
this,
this.config,
this.db,
);
this.config.on('change', (changedConfig) => { this.config.on('change', (changedConfig) => {
if (changedConfig.appUpdatePollInterval) { if (changedConfig.appUpdatePollInterval) {
@ -1240,7 +1234,7 @@ export class ApplicationManager extends EventEmitter {
if (maybeTrx != null) { if (maybeTrx != null) {
promise = setInTransaction(filteredApps, maybeTrx); promise = setInTransaction(filteredApps, maybeTrx);
} else { } else {
promise = this.db.transaction(setInTransaction); promise = db.transaction(setInTransaction);
} }
return promise return promise
.then(() => { .then(() => {

View File

@ -5,7 +5,7 @@ import * as _ from 'lodash';
import StrictEventEmitter from 'strict-event-emitter-types'; import StrictEventEmitter from 'strict-event-emitter-types';
import Config from '../config'; import Config from '../config';
import Database from '../db'; import * as db from '../db';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { import {
DeltaFetchOptions, DeltaFetchOptions,
@ -29,7 +29,6 @@ type ImageEventEmitter = StrictEventEmitter<EventEmitter, ImageEvents>;
interface ImageConstructOpts { interface ImageConstructOpts {
docker: DockerUtils; docker: DockerUtils;
logger: Logger; logger: Logger;
db: Database;
config: Config; config: Config;
} }
@ -61,7 +60,6 @@ type NormalisedDockerImage = Docker.ImageInfo & {
export class Images extends (EventEmitter as new () => ImageEventEmitter) { export class Images extends (EventEmitter as new () => ImageEventEmitter) {
private docker: DockerUtils; private docker: DockerUtils;
private logger: Logger; private logger: Logger;
private db: Database;
public appUpdatePollInterval: number; public appUpdatePollInterval: number;
@ -78,7 +76,6 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
this.docker = opts.docker; this.docker = opts.docker;
this.logger = opts.logger; this.logger = opts.logger;
this.db = opts.db;
} }
public async triggerFetch( public async triggerFetch(
@ -123,10 +120,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
await this.markAsSupervised(image); await this.markAsSupervised(image);
const img = await this.inspectByName(image.name); const img = await this.inspectByName(image.name);
await this.db await db.models('image').update({ dockerImageId: img.Id }).where(image);
.models('image')
.update({ dockerImageId: img.Id })
.where(image);
onFinish(true); onFinish(true);
return null; return null;
@ -150,10 +144,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
id = await this.fetchImage(image, opts, onProgress); id = await this.fetchImage(image, opts, onProgress);
} }
await this.db await db.models('image').update({ dockerImageId: id }).where(image);
.models('image')
.update({ dockerImageId: id })
.where(image);
this.logger.logSystemEvent(LogTypes.downloadImageSuccess, { image }); this.logger.logSystemEvent(LogTypes.downloadImageSuccess, { image });
success = true; success = true;
@ -194,7 +185,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
} }
public async getByDockerId(id: string): Promise<Image> { public async getByDockerId(id: string): Promise<Image> {
return await this.db.models('image').where({ dockerImageId: id }).first(); return await db.models('image').where({ dockerImageId: id }).first();
} }
public async removeByDockerId(id: string): Promise<void> { public async removeByDockerId(id: string): Promise<void> {
@ -218,7 +209,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
newImage.NormalisedRepoTags = await this.getNormalisedTags(image); newImage.NormalisedRepoTags = await this.getNormalisedTags(image);
return newImage; return newImage;
}), }),
this.db.models('image').select(), db.models('image').select(),
]); ]);
return cb(normalisedImages, dbImages); return cb(normalisedImages, dbImages);
} }
@ -292,7 +283,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
); );
if (id != null) { if (id != null) {
await this.db await db
.models('image') .models('image')
.update({ dockerImageId: id }) .update({ dockerImageId: id })
.where(supervisedImage); .where(supervisedImage);
@ -307,7 +298,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
); );
const ids = _(imagesToRemove).map('id').compact().value(); const ids = _(imagesToRemove).map('id').compact().value();
await this.db.models('image').del().whereIn('id', ids); await db.models('image').del().whereIn('id', ids);
} }
public async getStatus() { public async getStatus() {
@ -327,7 +318,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
public async update(image: Image): Promise<void> { public async update(image: Image): Promise<void> {
const formattedImage = this.format(image); const formattedImage = this.format(image);
await this.db await db
.models('image') .models('image')
.update(formattedImage) .update(formattedImage)
.where({ name: formattedImage.name }); .where({ name: formattedImage.name });
@ -350,7 +341,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
] = await Promise.all([ ] = await Promise.all([
this.docker.getRegistryAndName(constants.supervisorImage), this.docker.getRegistryAndName(constants.supervisorImage),
this.docker.getImage(constants.supervisorImage).inspect(), this.docker.getImage(constants.supervisorImage).inspect(),
this.db db
.models('image') .models('image')
.select('dockerImageId') .select('dockerImageId')
.then((vals) => vals.map((img: Image) => img.dockerImageId)), .then((vals) => vals.map((img: Image) => img.dockerImageId)),
@ -417,11 +408,11 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
const digest = imageName.split('@')[1]; const digest = imageName.split('@')[1];
let imagesFromDb: Image[]; let imagesFromDb: Image[];
if (digest != null) { if (digest != null) {
imagesFromDb = await this.db imagesFromDb = await db
.models('image') .models('image')
.where('name', 'like', `%@${digest}`); .where('name', 'like', `%@${digest}`);
} else { } else {
imagesFromDb = await this.db imagesFromDb = await db
.models('image') .models('image')
.where({ name: imageName }) .where({ name: imageName })
.select(); .select();
@ -496,7 +487,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
// We first fetch the image from the DB to ensure it exists, // We first fetch the image from the DB to ensure it exists,
// and get the dockerImageId and any other missing fields // and get the dockerImageId and any other missing fields
const images = await this.db.models('image').select().where(image); const images = await db.models('image').select().where(image);
if (images.length === 0) { if (images.length === 0) {
removed = false; removed = false;
@ -510,7 +501,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
await this.docker.getImage(img.name).remove({ force: true }); await this.docker.getImage(img.name).remove({ force: true });
removed = true; removed = true;
} else { } else {
const imagesFromDb = await this.db const imagesFromDb = await db
.models('image') .models('image')
.where({ dockerImageId: img.dockerImageId }) .where({ dockerImageId: img.dockerImageId })
.select(); .select();
@ -556,7 +547,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
this.reportChange(image.imageId); this.reportChange(image.imageId);
} }
await this.db.models('image').del().where({ id: img.id }); await db.models('image').del().where({ id: img.id });
if (removed) { if (removed) {
this.logger.logSystemEvent(LogTypes.deleteImageSuccess, { image }); this.logger.logSystemEvent(LogTypes.deleteImageSuccess, { image });
@ -565,7 +556,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
private async markAsSupervised(image: Image): Promise<void> { private async markAsSupervised(image: Image): Promise<void> {
const formattedImage = this.format(image); const formattedImage = this.format(image);
await this.db.upsertModel( await db.upsertModel(
'image', 'image',
formattedImage, formattedImage,
// TODO: Upsert to new values only when they already match? This is likely a bug // TODO: Upsert to new values only when they already match? This is likely a bug

View File

@ -15,14 +15,13 @@ import * as FnSchema from './functions';
import * as Schema from './schema'; import * as Schema from './schema';
import { SchemaReturn, SchemaTypeKey, schemaTypes } from './schema-type'; import { SchemaReturn, SchemaTypeKey, schemaTypes } from './schema-type';
import DB from '../db'; import * as db from '../db';
import { import {
ConfigurationValidationError, ConfigurationValidationError,
InternalInconsistencyError, InternalInconsistencyError,
} from '../lib/errors'; } from '../lib/errors';
interface ConfigOpts { interface ConfigOpts {
db: DB;
configPath?: string; configPath?: string;
} }
@ -44,12 +43,10 @@ interface ConfigEvents {
type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEvents>; type ConfigEventEmitter = StrictEventEmitter<EventEmitter, ConfigEvents>;
export class Config extends (EventEmitter as new () => ConfigEventEmitter) { export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
private db: DB;
private configJsonBackend: ConfigJsonConfigBackend; private configJsonBackend: ConfigJsonConfigBackend;
public constructor({ db, configPath }: ConfigOpts) { public constructor({ configPath }: ConfigOpts = {}) {
super(); super();
this.db = db;
this.configJsonBackend = new ConfigJsonConfigBackend( this.configJsonBackend = new ConfigJsonConfigBackend(
Schema.schema, Schema.schema,
configPath, configPath,
@ -64,13 +61,13 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
key: T, key: T,
trx?: Transaction, trx?: Transaction,
): Bluebird<SchemaReturn<T>> { ): Bluebird<SchemaReturn<T>> {
const db = trx || this.db.models.bind(this.db); const $db = trx || db.models.bind(db);
return Bluebird.try(() => { return Bluebird.try(() => {
if (Schema.schema.hasOwnProperty(key)) { if (Schema.schema.hasOwnProperty(key)) {
const schemaKey = key as Schema.SchemaKey; const schemaKey = key as Schema.SchemaKey;
return this.getSchema(schemaKey, db).then((value) => { return this.getSchema(schemaKey, $db).then((value) => {
if (value == null) { if (value == null) {
const defaultValue = schemaTypes[key].default; const defaultValue = schemaTypes[key].default;
if (defaultValue instanceof t.Type) { if (defaultValue instanceof t.Type) {
@ -159,12 +156,7 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
const strValue = Config.valueToString(value, key); const strValue = Config.valueToString(value, key);
if (oldValues[key] !== value) { if (oldValues[key] !== value) {
await this.db.upsertModel( await db.upsertModel('config', { key, value: strValue }, { key }, tx);
'config',
{ key, value: strValue },
{ key },
tx,
);
} }
}); });
@ -184,9 +176,7 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
if (trx != null) { if (trx != null) {
await setValuesInTransaction(trx); await setValuesInTransaction(trx);
} else { } else {
await this.db.transaction((tx: Transaction) => await db.transaction((tx: Transaction) => setValuesInTransaction(tx));
setValuesInTransaction(tx),
);
} }
this.emit('change', keyValues as ConfigMap<SchemaTypeKey>); this.emit('change', keyValues as ConfigMap<SchemaTypeKey>);
} }
@ -198,7 +188,7 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
if (Schema.schema[key].source === 'config.json') { if (Schema.schema[key].source === 'config.json') {
return this.configJsonBackend.remove(key); return this.configJsonBackend.remove(key);
} else if (Schema.schema[key].source === 'db') { } else if (Schema.schema[key].source === 'db') {
await this.db.models('config').del().where({ key }); await db.models('config').del().where({ key });
} else { } else {
throw new Error( throw new Error(
`Unknown or unsupported config backend: ${Schema.schema[key].source}`, `Unknown or unsupported config backend: ${Schema.schema[key].source}`,
@ -236,7 +226,7 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
private async getSchema<T extends Schema.SchemaKey>( private async getSchema<T extends Schema.SchemaKey>(
key: T, key: T,
db: Transaction, $db: Transaction,
): Promise<unknown> { ): Promise<unknown> {
let value: unknown; let value: unknown;
switch (Schema.schema[key].source) { switch (Schema.schema[key].source) {
@ -244,7 +234,7 @@ export class Config extends (EventEmitter as new () => ConfigEventEmitter) {
value = await this.configJsonBackend.get(key); value = await this.configJsonBackend.get(key);
break; break;
case 'db': case 'db':
const [conf] = await db('config').select('value').where({ key }); const [conf] = await $db('config').select('value').where({ key });
if (conf != null) { if (conf != null) {
return conf.value; return conf.value;
} }

View File

@ -3,61 +3,50 @@ import * as path from 'path';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
interface DBOpts {
databasePath?: string;
}
type DBTransactionCallback = (trx: Knex.Transaction) => void; type DBTransactionCallback = (trx: Knex.Transaction) => void;
export type Transaction = Knex.Transaction; export type Transaction = Knex.Transaction;
export class DB { const databasePath = constants.databasePath;
private databasePath: string; const knex = Knex({
private knex: Knex;
public constructor({ databasePath }: DBOpts = {}) {
this.databasePath = databasePath || constants.databasePath;
this.knex = Knex({
client: 'sqlite3', client: 'sqlite3',
connection: { connection: {
filename: this.databasePath, filename: databasePath,
}, },
useNullAsDefault: true, useNullAsDefault: true,
}); });
}
public async init(): Promise<void> { export const initialized = (async () => {
try { try {
await this.knex('knex_migrations_lock').update({ is_locked: 0 }); await knex('knex_migrations_lock').update({ is_locked: 0 });
} catch { } catch {
/* ignore */ /* ignore */
} }
return this.knex.migrate.latest({ return knex.migrate.latest({
directory: path.join(__dirname, 'migrations'), directory: path.join(__dirname, 'migrations'),
}); });
} })();
public models(modelName: string): Knex.QueryBuilder { export function models(modelName: string): Knex.QueryBuilder {
return this.knex(modelName); return knex(modelName);
} }
public async upsertModel( export async function upsertModel(
modelName: string, modelName: string,
obj: any, obj: any,
id: Dictionary<unknown>, id: Dictionary<unknown>,
trx?: Knex.Transaction, trx?: Knex.Transaction,
): Promise<any> { ): Promise<any> {
const knex = trx || this.knex; const k = trx || knex;
const n = await knex(modelName).update(obj).where(id); const n = await k(modelName).update(obj).where(id);
if (n === 0) { if (n === 0) {
return knex(modelName).insert(obj); return k(modelName).insert(obj);
}
}
public transaction(cb: DBTransactionCallback): Promise<Knex.Transaction> {
return this.knex.transaction(cb);
} }
} }
export default DB; export function transaction(
cb: DBTransactionCallback,
): Promise<Knex.Transaction> {
return knex.transaction(cb);
}

View File

@ -32,7 +32,7 @@ declare function serviceAction(
declare function safeStateClone( declare function safeStateClone(
targetState: InstancedDeviceState, targetState: InstancedDeviceState,
// Use an any here, because it's not an InstancedDeviceState, ): // Use an any here, because it's not an InstancedDeviceState,
// and it's also not the exact same type as the API serves from // and it's also not the exact same type as the API serves from
// state endpoint (more details in the function) // state endpoint (more details in the function)
): Dictionary<any>; Dictionary<any>;

View File

@ -5,6 +5,7 @@ import * as _ from 'lodash';
import { ApplicationManager } from '../application-manager'; import { ApplicationManager } from '../application-manager';
import { Service } from '../compose/service'; import { Service } from '../compose/service';
import Volume from '../compose/volume'; import Volume from '../compose/volume';
import * as db from '../db';
import { spawnJournalctl } from '../lib/journald'; import { spawnJournalctl } from '../lib/journald';
import { import {
appNotFoundMessage, appNotFoundMessage,
@ -150,7 +151,7 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
Bluebird.join( Bluebird.join(
applications.services.getStatus(), applications.services.getStatus(),
applications.images.getStatus(), applications.images.getStatus(),
applications.db.models('app').select(['appId', 'commit', 'name']), db.models('app').select(['appId', 'commit', 'name']),
( (
services, services,
images, images,

View File

@ -3,7 +3,7 @@ import { inspect } from 'util';
import Config from './config'; import Config from './config';
import { SchemaTypeKey } from './config/schema-type'; import { SchemaTypeKey } from './config/schema-type';
import Database, { Transaction } from './db'; import * as db from './db';
import Logger from './logger'; import Logger from './logger';
import { ConfigOptions, DeviceConfigBackend } from './config/backend'; import { ConfigOptions, DeviceConfigBackend } from './config/backend';
@ -17,7 +17,6 @@ import { DeviceStatus } from './types/state';
const vpnServiceName = 'openvpn'; const vpnServiceName = 'openvpn';
interface DeviceConfigConstructOpts { interface DeviceConfigConstructOpts {
db: Database;
config: Config; config: Config;
logger: Logger; logger: Logger;
} }
@ -57,7 +56,6 @@ interface DeviceActionExecutors {
} }
export class DeviceConfig { export class DeviceConfig {
private db: Database;
private config: Config; private config: Config;
private logger: Logger; private logger: Logger;
private rebootRequired = false; private rebootRequired = false;
@ -150,8 +148,7 @@ export class DeviceConfig {
}, },
}; };
public constructor({ db, config, logger }: DeviceConfigConstructOpts) { public constructor({ config, logger }: DeviceConfigConstructOpts) {
this.db = db;
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
@ -233,9 +230,9 @@ export class DeviceConfig {
public async setTarget( public async setTarget(
target: Dictionary<string>, target: Dictionary<string>,
trx?: Transaction, trx?: db.Transaction,
): Promise<void> { ): Promise<void> {
const db = trx != null ? trx : this.db.models.bind(this.db); const $db = trx ?? db.models.bind(db);
const formatted = await this.formatConfigKeys(target); const formatted = await this.formatConfigKeys(target);
// check for legacy keys // check for legacy keys
@ -247,13 +244,13 @@ export class DeviceConfig {
targetValues: JSON.stringify(formatted), targetValues: JSON.stringify(formatted),
}; };
await db('deviceConfig').update(confToUpdate); await $db('deviceConfig').update(confToUpdate);
} }
public async getTarget({ initial = false }: { initial?: boolean } = {}) { public async getTarget({ initial = false }: { initial?: boolean } = {}) {
const [unmanaged, [devConfig]] = await Promise.all([ const [unmanaged, [devConfig]] = await Promise.all([
this.config.get('unmanaged'), this.config.get('unmanaged'),
this.db.models('deviceConfig').select('targetValues'), db.models('deviceConfig').select('targetValues'),
]); ]);
let conf: Dictionary<string>; let conf: Dictionary<string>;

View File

@ -8,7 +8,7 @@ import StrictEventEmitter from 'strict-event-emitter-types';
import prettyMs = require('pretty-ms'); import prettyMs = require('pretty-ms');
import Config, { ConfigType } from './config'; import Config, { ConfigType } from './config';
import Database from './db'; import * as db from './db';
import EventTracker from './event-tracker'; import EventTracker from './event-tracker';
import Logger from './logger'; import Logger from './logger';
@ -176,7 +176,6 @@ function createDeviceStateRouter(deviceState: DeviceState) {
} }
interface DeviceStateConstructOpts { interface DeviceStateConstructOpts {
db: Database;
config: Config; config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
logger: Logger; logger: Logger;
@ -219,7 +218,6 @@ type DeviceStateStep<T extends PossibleStepTargets> =
| ConfigStep; | ConfigStep;
export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) { export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmitter) {
public db: Database;
public config: Config; public config: Config;
public eventTracker: EventTracker; public eventTracker: EventTracker;
public logger: Logger; public logger: Logger;
@ -246,26 +244,22 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
public router: express.Router; public router: express.Router;
constructor({ constructor({
db,
config, config,
eventTracker, eventTracker,
logger, logger,
apiBinder, apiBinder,
}: DeviceStateConstructOpts) { }: DeviceStateConstructOpts) {
super(); super();
this.db = db;
this.config = config; this.config = config;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.logger = logger; this.logger = logger;
this.deviceConfig = new DeviceConfig({ this.deviceConfig = new DeviceConfig({
db: this.db,
config: this.config, config: this.config,
logger: this.logger, logger: this.logger,
}); });
this.applications = new ApplicationManager({ this.applications = new ApplicationManager({
config: this.config, config: this.config,
logger: this.logger, logger: this.logger,
db: this.db,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
deviceState: this, deviceState: this,
apiBinder, apiBinder,
@ -463,7 +457,7 @@ export class DeviceState extends (EventEmitter as new () => DeviceStateEventEmit
const apiEndpoint = await this.config.get('apiEndpoint'); const apiEndpoint = await this.config.get('apiEndpoint');
await this.usingWriteLockTarget(async () => { await this.usingWriteLockTarget(async () => {
await this.db.transaction(async (trx) => { await db.transaction(async (trx) => {
await this.config.set({ name: target.local.name }, trx); await this.config.set({ name: target.local.name }, trx);
await this.deviceConfig.setTarget(target.local.config, trx); await this.deviceConfig.setTarget(target.local.config, trx);

View File

@ -1,6 +1,6 @@
import { endsWith, map } from 'lodash'; import { endsWith, map } from 'lodash';
import TypedError = require('typed-error');
import { Response } from 'request'; import { Response } from 'request';
import TypedError = require('typed-error');
import { checkInt } from './validation'; import { checkInt } from './validation';

View File

@ -11,7 +11,7 @@ const rimrafAsync = Bluebird.promisify(rimraf);
import { ApplicationManager } from '../application-manager'; import { ApplicationManager } from '../application-manager';
import Config from '../config'; import Config from '../config';
import Database, { Transaction } from '../db'; import * as db from '../db';
import DeviceState from '../device-state'; import DeviceState from '../device-state';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors'; import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors';
@ -108,7 +108,6 @@ export function convertLegacyAppsJson(appsArray: any[]): AppsJsonFormat {
export async function normaliseLegacyDatabase( export async function normaliseLegacyDatabase(
config: Config, config: Config,
application: ApplicationManager, application: ApplicationManager,
db: Database,
balenaApi: PinejsClientRequest, balenaApi: PinejsClientRequest,
) { ) {
// When legacy apps are present, we kill their containers and migrate their /data to a named volume // When legacy apps are present, we kill their containers and migrate their /data to a named volume
@ -196,7 +195,7 @@ export async function normaliseLegacyDatabase(
.where({ name: service.image }) .where({ name: service.image })
.select(); .select();
await db.transaction(async (trx: Transaction) => { await db.transaction(async (trx: db.Transaction) => {
try { try {
if (imagesFromDatabase.length > 0) { if (imagesFromDatabase.length > 0) {
log.debug('Deleting existing image entry in db'); log.debug('Deleting existing image entry in db');

View File

@ -3,7 +3,7 @@ import * as Docker from 'dockerode';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Config from './config'; import Config from './config';
import Database from './db'; import * as db from './db';
import * as constants from './lib/constants'; import * as constants from './lib/constants';
import { SupervisorContainerNotFoundError } from './lib/errors'; import { SupervisorContainerNotFoundError } from './lib/errors';
import log from './lib/supervisor-console'; import log from './lib/supervisor-console';
@ -74,7 +74,6 @@ export class LocalModeManager {
public config: Config, public config: Config,
public docker: Docker, public docker: Docker,
public logger: Logger, public logger: Logger,
public db: Database,
private containerId: string | undefined = constants.containerId, private containerId: string | undefined = constants.containerId,
) {} ) {}
@ -182,7 +181,7 @@ export class LocalModeManager {
} }
private async cleanEngineSnapshots() { private async cleanEngineSnapshots() {
await this.db.models('engineSnapshot').delete(); await db.models('engineSnapshot').delete();
} }
// Store engine snapshot data in the local database. // Store engine snapshot data in the local database.
@ -192,7 +191,7 @@ export class LocalModeManager {
`Storing engine snapshot in the database. Timestamp: ${timestamp}`, `Storing engine snapshot in the database. Timestamp: ${timestamp}`,
); );
await this.cleanEngineSnapshots(); await this.cleanEngineSnapshots();
await this.db.models('engineSnapshot').insert({ await db.models('engineSnapshot').insert({
snapshot: JSON.stringify(record.snapshot), snapshot: JSON.stringify(record.snapshot),
timestamp, timestamp,
}); });
@ -210,7 +209,7 @@ export class LocalModeManager {
// Read the latest stored snapshot from the database. // Read the latest stored snapshot from the database.
public async retrieveLatestSnapshot(): Promise<EngineSnapshotRecord | null> { public async retrieveLatestSnapshot(): Promise<EngineSnapshotRecord | null> {
const r = await this.db const r = await db
.models('engineSnapshot') .models('engineSnapshot')
.select() .select()
.orderBy('rowid', 'DESC') .orderBy('rowid', 'DESC')
@ -263,7 +262,7 @@ export class LocalModeManager {
}); });
// Remove any local mode state added to the database. // Remove any local mode state added to the database.
await this.db await db
.models('app') .models('app')
.del() .del()
.where({ source: 'local' }) .where({ source: 'local' })

View File

@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Config, { ConfigType } from './config'; import Config, { ConfigType } from './config';
import DB from './db'; import * as db from './db';
import { EventTracker } from './event-tracker'; import { EventTracker } from './event-tracker';
import Docker from './lib/docker-utils'; import Docker from './lib/docker-utils';
import { LogType } from './lib/log-types'; import { LogType } from './lib/log-types';
@ -33,7 +33,6 @@ interface LoggerSetupOptions {
type LogEventObject = Dictionary<any> | null; type LogEventObject = Dictionary<any> | null;
interface LoggerConstructOptions { interface LoggerConstructOptions {
db: DB;
eventTracker: EventTracker; eventTracker: EventTracker;
} }
@ -43,15 +42,13 @@ export class Logger {
private localBackend: LocalLogBackend | null = null; private localBackend: LocalLogBackend | null = null;
private eventTracker: EventTracker; private eventTracker: EventTracker;
private db: DB;
private containerLogs: { [containerId: string]: ContainerLogs } = {}; private containerLogs: { [containerId: string]: ContainerLogs } = {};
private logMonitor: LogMonitor; private logMonitor: LogMonitor;
public constructor({ db, eventTracker }: LoggerConstructOptions) { public constructor({ eventTracker }: LoggerConstructOptions) {
this.backend = null; this.backend = null;
this.eventTracker = eventTracker; this.eventTracker = eventTracker;
this.db = db; this.logMonitor = new LogMonitor();
this.logMonitor = new LogMonitor(db);
} }
public init({ public init({
@ -247,7 +244,7 @@ export class Logger {
public async clearOutOfDateDBLogs(containerIds: string[]) { public async clearOutOfDateDBLogs(containerIds: string[]) {
log.debug('Performing database cleanup for container log timestamps'); log.debug('Performing database cleanup for container log timestamps');
await this.db await db
.models('containerLogs') .models('containerLogs')
.whereNotIn('containerId', containerIds) .whereNotIn('containerId', containerIds)
.delete(); .delete();

View File

@ -1,6 +1,6 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import Database from '../db'; import * as db from '../db';
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
@ -15,7 +15,7 @@ export class LogMonitor {
private timestamps: { [containerId: string]: number } = {}; private timestamps: { [containerId: string]: number } = {};
private writeRequired: { [containerId: string]: boolean } = {}; private writeRequired: { [containerId: string]: boolean } = {};
public constructor(private db: Database) { public constructor() {
setInterval(() => this.flushDb(), DB_FLUSH_INTERVAL); setInterval(() => this.flushDb(), DB_FLUSH_INTERVAL);
} }
@ -35,7 +35,7 @@ export class LogMonitor {
// db to avoid multiple db actions at once // db to avoid multiple db actions at once
this.timestamps[containerId] = 0; this.timestamps[containerId] = 0;
try { try {
const timestampObj = await this.db const timestampObj = await db
.models('containerLogs') .models('containerLogs')
.select('lastSentTimestamp') .select('lastSentTimestamp')
.where({ containerId }); .where({ containerId });
@ -43,7 +43,7 @@ export class LogMonitor {
if (timestampObj == null || _.isEmpty(timestampObj)) { if (timestampObj == null || _.isEmpty(timestampObj)) {
// Create a row in the db so there's something to // Create a row in the db so there's something to
// update // update
await this.db await db
.models('containerLogs') .models('containerLogs')
.insert({ containerId, lastSentTimestamp: 0 }); .insert({ containerId, lastSentTimestamp: 0 });
} else { } else {
@ -69,7 +69,7 @@ export class LogMonitor {
continue; continue;
} }
await this.db await db
.models('containerLogs') .models('containerLogs')
.where({ containerId }) .where({ containerId })
.update({ lastSentTimestamp: this.timestamps[containerId] }); .update({ lastSentTimestamp: this.timestamps[containerId] });

View File

@ -15,6 +15,7 @@ import * as bodyParser from 'body-parser';
import * as url from 'url'; import * as url from 'url';
import { log } from './lib/supervisor-console'; import { log } from './lib/supervisor-console';
import * as db from './db';
const mkdirpAsync = Promise.promisify(mkdirp); const mkdirpAsync = Promise.promisify(mkdirp);
@ -80,20 +81,18 @@ const formatCurrentAsState = (device) => ({
}); });
const createProxyvisorRouter = function (proxyvisor) { const createProxyvisorRouter = function (proxyvisor) {
const { db } = proxyvisor;
const router = express.Router(); const router = express.Router();
router.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); router.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
router.use(bodyParser.json({ limit: '10mb' })); router.use(bodyParser.json({ limit: '10mb' }));
router.get('/v1/devices', (_req, res) => router.get('/v1/devices', async (_req, res) => {
db try {
.models('dependentDevice') const fields = await db.models('dependentDevice').select();
.select() const devices = fields.map(parseDeviceFields);
.map(parseDeviceFields) res.json(devices);
.then((devices) => res.json(devices)) } catch (err) {
.catch((err) => res.status(503).send(err?.message || err || 'Unknown error');
res.status(503).send(err?.message || err || 'Unknown error'), }
), });
);
router.post('/v1/devices', function (req, res) { router.post('/v1/devices', function (req, res) {
let { appId, device_type } = req.body; let { appId, device_type } = req.body;
@ -297,54 +296,54 @@ const createProxyvisorRouter = function (proxyvisor) {
}); });
}); });
router.get('/v1/dependent-apps/:appId/assets/:commit', (req, res) => router.get('/v1/dependent-apps/:appId/assets/:commit', async (req, res) => {
db try {
const [app] = await db
.models('dependentApp') .models('dependentApp')
.select() .select()
.where(_.pick(req.params, 'appId', 'commit')) .where(_.pick(req.params, 'appId', 'commit'));
.then(function ([app]) {
if (!app) { if (!app) {
return res.status(404).send('Not found'); return res.status(404).send('Not found');
} }
const dest = tarPath(app.appId, app.commit); const dest = tarPath(app.appId, app.commit);
return fs try {
.lstat(dest) await fs.lstat(dest);
.catch(() => } catch {
Promise.using( await Promise.using(
proxyvisor.docker.imageRootDirMounted(app.image), proxyvisor.docker.imageRootDirMounted(app.image),
(rootDir) => getTarArchive(rootDir + '/assets', dest), (rootDir) => getTarArchive(rootDir + '/assets', dest),
), );
) }
.then(() => res.sendFile(dest)); res.sendFile(dest);
}) } catch (err) {
.catch(function (err) {
log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err);
return res.status(503).send(err?.message || err || 'Unknown error'); return res.status(503).send(err?.message || err || 'Unknown error');
}), }
); });
router.get('/v1/dependent-apps', (req, res) => router.get('/v1/dependent-apps', async (req, res) => {
db try {
.models('dependentApp') const apps = await db.models('dependentApp').select();
.select()
.map((app) => ({ const $apps = apps.map((app) => ({
id: parseInt(app.appId, 10), id: parseInt(app.appId, 10),
commit: app.commit, commit: app.commit,
name: app.name, name: app.name,
config: JSON.parse(app.config ?? '{}'), config: JSON.parse(app.config ?? '{}'),
})) }));
.then((apps) => res.json(apps)) res.json($apps);
.catch(function (err) { } catch (err) {
log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err);
return res.status(503).send(err?.message || err || 'Unknown error'); return res.status(503).send(err?.message || err || 'Unknown error');
}), }
); });
return router; return router;
}; };
export class Proxyvisor { export class Proxyvisor {
constructor({ config, logger, db, docker, images, applications }) { constructor({ config, logger, docker, images, applications }) {
this.bindToAPI = this.bindToAPI.bind(this); this.bindToAPI = this.bindToAPI.bind(this);
this.executeStepAction = this.executeStepAction.bind(this); this.executeStepAction = this.executeStepAction.bind(this);
this.getCurrentStates = this.getCurrentStates.bind(this); this.getCurrentStates = this.getCurrentStates.bind(this);
@ -362,7 +361,6 @@ export class Proxyvisor {
this.sendUpdates = this.sendUpdates.bind(this); this.sendUpdates = this.sendUpdates.bind(this);
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.db = db;
this.docker = docker; this.docker = docker;
this.images = images; this.images = images;
this.applications = applications; this.applications = applications;
@ -392,7 +390,7 @@ export class Proxyvisor {
device.apps[appId].environment, device.apps[appId].environment,
); );
const targetConfig = JSON.stringify(device.apps[appId].config); const targetConfig = JSON.stringify(device.apps[appId].config);
return this.db return db
.models('dependentDevice') .models('dependentDevice')
.update({ .update({
appId, appId,
@ -423,14 +421,12 @@ export class Proxyvisor {
targetConfig, targetConfig,
targetEnvironment, targetEnvironment,
}; };
return this.db return db.models('dependentDevice').insert(deviceForDB);
.models('dependentDevice')
.insert(deviceForDB);
}); });
}); });
}) })
.then(() => { .then(() => {
return this.db return db
.models('dependentDevice') .models('dependentDevice')
.where({ appId: step.appId }) .where({ appId: step.appId })
.whereNotIn('uuid', _.map(step.devices, 'uuid')) .whereNotIn('uuid', _.map(step.devices, 'uuid'))
@ -440,7 +436,7 @@ export class Proxyvisor {
return this.normaliseDependentAppForDB(step.app); return this.normaliseDependentAppForDB(step.app);
}) })
.then((appForDB) => { .then((appForDB) => {
return this.db.upsertModel('dependentApp', appForDB, { return db.upsertModel('dependentApp', appForDB, {
appId: step.appId, appId: step.appId,
}); });
}) })
@ -478,7 +474,7 @@ export class Proxyvisor {
removeDependentApp: (step) => { removeDependentApp: (step) => {
// find step.app and delete it from the DB // find step.app and delete it from the DB
// find devices with step.appId and delete them from the DB // find devices with step.appId and delete them from the DB
return this.db.transaction((trx) => return db.transaction((trx) =>
trx('dependentApp') trx('dependentApp')
.where({ appId: step.appId }) .where({ appId: step.appId })
.del() .del()
@ -509,10 +505,10 @@ export class Proxyvisor {
getCurrentStates() { getCurrentStates() {
return Promise.join( return Promise.join(
Promise.map( Promise.map(
this.db.models('dependentApp').select(), db.models('dependentApp').select(),
this.normaliseDependentAppFromDB, this.normaliseDependentAppFromDB,
), ),
this.db.models('dependentDevice').select(), db.models('dependentDevice').select(),
function (apps, devicesFromDB) { function (apps, devicesFromDB) {
const devices = _.map(devicesFromDB, function (device) { const devices = _.map(devicesFromDB, function (device) {
const dev = { const dev = {
@ -590,7 +586,7 @@ export class Proxyvisor {
return Promise.map(appsArray, this.normaliseDependentAppForDB) return Promise.map(appsArray, this.normaliseDependentAppForDB)
.tap((appsForDB) => { .tap((appsForDB) => {
return Promise.map(appsForDB, (app) => { return Promise.map(appsForDB, (app) => {
return this.db.upsertModel( return db.upsertModel(
'dependentAppTarget', 'dependentAppTarget',
app, app,
{ appId: app.appId }, { appId: app.appId },
@ -619,7 +615,7 @@ export class Proxyvisor {
); );
}).then((devicesForDB) => { }).then((devicesForDB) => {
return Promise.map(devicesForDB, (device) => { return Promise.map(devicesForDB, (device) => {
return this.db.upsertModel( return db.upsertModel(
'dependentDeviceTarget', 'dependentDeviceTarget',
device, device,
{ uuid: device.uuid }, { uuid: device.uuid },
@ -686,11 +682,11 @@ export class Proxyvisor {
getTarget() { getTarget() {
return Promise.props({ return Promise.props({
apps: Promise.map( apps: Promise.map(
this.db.models('dependentAppTarget').select(), db.models('dependentAppTarget').select(),
this.normaliseDependentAppFromDB, this.normaliseDependentAppFromDB,
), ),
devices: Promise.map( devices: Promise.map(
this.db.models('dependentDeviceTarget').select(), db.models('dependentDeviceTarget').select(),
this.normaliseDependentDeviceTargetFromDB, this.normaliseDependentDeviceTargetFromDB,
), ),
}); });
@ -899,7 +895,7 @@ export class Proxyvisor {
} }
getHookEndpoint(appId) { getHookEndpoint(appId) {
return this.db return db
.models('dependentApp') .models('dependentApp')
.select('parentApp') .select('parentApp')
.where({ appId }) .where({ appId })
@ -958,7 +954,7 @@ export class Proxyvisor {
.timeout(timeout) .timeout(timeout)
.spread((response, body) => { .spread((response, body) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
return this.db.models('dependentDevice').del().where({ uuid }); return db.models('dependentDevice').del().where({ uuid });
} else { } else {
throw new Error(`Hook returned ${response.statusCode}: ${body}`); throw new Error(`Hook returned ${response.statusCode}: ${body}`);
} }
@ -968,7 +964,7 @@ export class Proxyvisor {
sendUpdates({ uuid }) { sendUpdates({ uuid }) {
return Promise.join( return Promise.join(
this.db.models('dependentDevice').where({ uuid }).select(), db.models('dependentDevice').where({ uuid }).select(),
this.config.get('apiTimeout'), this.config.get('apiTimeout'),
([dev], apiTimeout) => { ([dev], apiTimeout) => {
if (dev == null) { if (dev == null) {

View File

@ -1,6 +1,6 @@
import APIBinder from './api-binder'; import APIBinder from './api-binder';
import Config, { ConfigKey } from './config'; import Config, { ConfigKey } from './config';
import Database from './db'; import * as db from './db';
import DeviceState from './device-state'; import DeviceState from './device-state';
import EventTracker from './event-tracker'; import EventTracker from './event-tracker';
import { intialiseContractRequirements } from './lib/contracts'; import { intialiseContractRequirements } from './lib/contracts';
@ -29,7 +29,6 @@ const startupConfigFields: ConfigKey[] = [
]; ];
export class Supervisor { export class Supervisor {
private db: Database;
private config: Config; private config: Config;
private eventTracker: EventTracker; private eventTracker: EventTracker;
private logger: Logger; private logger: Logger;
@ -38,19 +37,16 @@ export class Supervisor {
private api: SupervisorAPI; private api: SupervisorAPI;
public constructor() { public constructor() {
this.db = new Database(); this.config = new Config();
this.config = new Config({ db: this.db });
this.eventTracker = new EventTracker(); this.eventTracker = new EventTracker();
this.logger = new Logger({ db: this.db, eventTracker: this.eventTracker }); this.logger = new Logger({ eventTracker: this.eventTracker });
this.apiBinder = new APIBinder({ this.apiBinder = new APIBinder({
config: this.config, config: this.config,
db: this.db,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
logger: this.logger, logger: this.logger,
}); });
this.deviceState = new DeviceState({ this.deviceState = new DeviceState({
config: this.config, config: this.config,
db: this.db,
eventTracker: this.eventTracker, eventTracker: this.eventTracker,
logger: this.logger, logger: this.logger,
apiBinder: this.apiBinder, apiBinder: this.apiBinder,
@ -76,7 +72,7 @@ export class Supervisor {
public async init() { public async init() {
log.info(`Supervisor v${version} starting up...`); log.info(`Supervisor v${version} starting up...`);
await this.db.init(); await db.initialized;
await this.config.init(); await this.config.init();
const conf = await this.config.getMany(startupConfigFields); const conf = await this.config.getMany(startupConfigFields);
@ -110,7 +106,6 @@ export class Supervisor {
await normaliseLegacyDatabase( await normaliseLegacyDatabase(
this.deviceState.config, this.deviceState.config,
this.deviceState.applications, this.deviceState.applications,
this.db,
this.apiBinder.balenaApi, this.apiBinder.balenaApi,
); );
} }

View File

@ -2,7 +2,7 @@ import * as _ from 'lodash';
import { ApplicationManager } from './application-manager'; import { ApplicationManager } from './application-manager';
import Config from './config'; import Config from './config';
import Database, { Transaction } from './db'; import * as db from './db';
// Once we have correct types for both applications and the // Once we have correct types for both applications and the
// incoming target state this should be changed // incoming target state this should be changed
@ -26,7 +26,6 @@ export class TargetStateAccessor {
public constructor( public constructor(
protected applications: ApplicationManager, protected applications: ApplicationManager,
protected config: Config, protected config: Config,
protected db: Database,
) { ) {
// If we switch backend, the target state also needs to // If we switch backend, the target state also needs to
// be invalidated (this includes switching to and from // be invalidated (this includes switching to and from
@ -56,23 +55,21 @@ export class TargetStateAccessor {
]); ]);
const source = localMode ? 'local' : apiEndpoint; const source = localMode ? 'local' : apiEndpoint;
this.targetState = await this.db.models('app').where({ source }); this.targetState = await db.models('app').where({ source });
} }
return this.targetState!; return this.targetState!;
} }
public async setTargetApps( public async setTargetApps(
apps: DatabaseApps, apps: DatabaseApps,
trx: Transaction, trx: db.Transaction,
): Promise<void> { ): Promise<void> {
// We can't cache the value here, as it could be for a // We can't cache the value here, as it could be for a
// different source // different source
this.targetState = undefined; this.targetState = undefined;
await Promise.all( await Promise.all(
apps.map((app) => apps.map((app) => db.upsertModel('app', app, { appId: app.appId }, trx)),
this.db.upsertModel('app', app, { appId: app.appId }, trx),
),
); );
} }
} }

View File

@ -9,6 +9,25 @@ process.env.LED_FILE = './test/data/led_file';
import * as dbus from 'dbus'; import * as dbus from 'dbus';
import { DBusError, DBusInterface } from 'dbus'; import { DBusError, DBusInterface } from 'dbus';
import { stub } from 'sinon'; import { stub } from 'sinon';
import * as fs from 'fs';
// Make sure they are no database files left over from
// previous runs
try {
fs.unlinkSync(process.env.DATABASE_PATH);
} catch {
/* noop */
}
try {
fs.unlinkSync(process.env.DATABASE_PATH_2);
} catch {
/* noop */
}
try {
fs.unlinkSync(process.env.DATABASE_PATH_3);
} catch {
/* noop */
}
stub(dbus, 'getBus').returns({ stub(dbus, 'getBus').returns({
getInterface: ( getInterface: (

View File

@ -1,12 +1,10 @@
import ChaiConfig = require('./lib/chai-config'); import ChaiConfig = require('./lib/chai-config');
import prepare = require('./lib/prepare');
const { expect } = ChaiConfig; const { expect } = ChaiConfig;
import constants = require('../src/lib/constants'); import constants = require('../src/lib/constants');
describe('constants', function () { describe('constants', function () {
before(() => prepare());
it('has the correct configJsonPathOnHost', () => it('has the correct configJsonPathOnHost', () =>
expect(constants.configJsonPathOnHost).to.equal('/config.json')); expect(constants.configJsonPathOnHost).to.equal('/config.json'));
it('has the correct rootMountPoint', () => it('has the correct rootMountPoint', () =>

View File

@ -5,7 +5,7 @@ import { fs } from 'mz';
import ChaiConfig = require('./lib/chai-config'); import ChaiConfig = require('./lib/chai-config');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
import { DB } from '../src/db'; import * as constants from '../src/lib/constants';
const { expect } = ChaiConfig; const { expect } = ChaiConfig;
@ -44,37 +44,37 @@ async function createOldDatabase(path: string) {
return knex; return knex;
} }
describe('DB', () => { describe('Database Migrations', () => {
let db: DB; before(async () => {
await prepare();
before(() => {
prepare();
db = new DB();
}); });
it('initializes correctly, running the migrations', () => { after(() => {
return expect(db.init()).to.be.fulfilled; // @ts-ignore
constants.databasePath = process.env.DATABASE_PATH;
delete require.cache[require.resolve('../src/db')];
}); });
it('creates a database at the path from an env var', () => { it('creates a database at the path passed on creation', async () => {
const promise = fs.stat(process.env.DATABASE_PATH!); const databasePath = process.env.DATABASE_PATH_2!;
return expect(promise).to.be.fulfilled; // @ts-ignore
}); constants.databasePath = databasePath;
delete require.cache[require.resolve('../src/db')];
it('creates a database at the path passed on creation', () => { const testDb = await import('../src/db');
const db2 = new DB({ databasePath: process.env.DATABASE_PATH_2 }); await testDb.initialized;
const promise = db2 await fs.stat(databasePath);
.init()
.then(() => fs.stat(process.env.DATABASE_PATH_2!));
return expect(promise).to.be.fulfilled;
}); });
it('adds new fields and removes old ones in an old database', async () => { it('adds new fields and removes old ones in an old database', async () => {
const databasePath = process.env.DATABASE_PATH_3!; const databasePath = process.env.DATABASE_PATH_3!;
const knexForDB = await createOldDatabase(databasePath); const knexForDB = await createOldDatabase(databasePath);
const testDb = new DB({ databasePath }); // @ts-ignore
await testDb.init(); constants.databasePath = databasePath;
delete require.cache[require.resolve('../src/db')];
const testDb = await import('../src/db');
await testDb.initialized;
await Bluebird.all([ await Bluebird.all([
expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true, expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true,
expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be
@ -97,7 +97,22 @@ describe('DB', () => {
.to.eventually.be.true, .to.eventually.be.true,
]); ]);
}); });
});
describe('Database', () => {
let db: typeof import('../src/db');
before(async () => {
await prepare();
db = await import('../src/db');
});
it('initializes correctly, running the migrations', () => {
return expect(db.initialized).to.be.fulfilled;
});
it('creates a database at the path from an env var', () => {
const promise = fs.stat(process.env.DATABASE_PATH!);
return expect(promise).to.be.fulfilled;
});
it('creates a deviceConfig table with a single default value', async () => { it('creates a deviceConfig table with a single default value', async () => {
const deviceConfig = await db.models('deviceConfig').select(); const deviceConfig = await db.models('deviceConfig').select();
expect(deviceConfig).to.have.lengthOf(1); expect(deviceConfig).to.have.lengthOf(1);

View File

@ -9,19 +9,15 @@ chai.use(require('chai-events'));
const { expect } = chai; const { expect } = chai;
import Config from '../src/config'; import Config from '../src/config';
import DB from '../src/db';
import constants = require('../src/lib/constants'); import constants = require('../src/lib/constants');
describe('Config', () => { describe('Config', () => {
let db: DB;
let conf: Config; let conf: Config;
before(async () => { before(async () => {
prepare(); await prepare();
db = new DB(); conf = new Config();
conf = new Config({ db });
await db.init();
await conf.init(); await conf.init();
}); });
@ -32,7 +28,7 @@ describe('Config', () => {
}); });
it('uses the correct config.json path from the root mount when passed as argument to the constructor', async () => { it('uses the correct config.json path from the root mount when passed as argument to the constructor', async () => {
const conf2 = new Config({ db, configPath: '/foo.json' }); const conf2 = new Config({ configPath: '/foo.json' });
expect(await (conf2 as any).configJsonBackend.path()).to.equal( expect(await (conf2 as any).configJsonBackend.path()).to.equal(
'test/data/foo.json', 'test/data/foo.json',
); );
@ -131,10 +127,8 @@ describe('Config', () => {
describe('Function config providers', () => { describe('Function config providers', () => {
before(async () => { before(async () => {
prepare(); await prepare();
db = new DB(); conf = new Config();
conf = new Config({ db });
await db.init();
await conf.init(); await conf.init();
}); });

View File

@ -11,7 +11,6 @@ const { expect } = chai;
import Config from '../src/config'; import Config from '../src/config';
import { RPiConfigBackend } from '../src/config/backend'; import { RPiConfigBackend } from '../src/config/backend';
import DB from '../src/db';
import DeviceState from '../src/device-state'; import DeviceState from '../src/device-state';
import { loadTargetFromFile } from '../src/device-state/preload'; import { loadTargetFromFile } from '../src/device-state/preload';
@ -209,8 +208,7 @@ const testTargetInvalid = {
}; };
describe('deviceState', () => { describe('deviceState', () => {
const db = new DB(); const config = new Config();
const config = new Config({ db });
const logger = { const logger = {
clearOutOfDateDBLogs() { clearOutOfDateDBLogs() {
/* noop */ /* noop */
@ -218,7 +216,7 @@ describe('deviceState', () => {
}; };
let deviceState: DeviceState; let deviceState: DeviceState;
before(async () => { before(async () => {
prepare(); await prepare();
const eventTracker = { const eventTracker = {
track: console.log, track: console.log,
}; };
@ -234,7 +232,6 @@ describe('deviceState', () => {
}); });
deviceState = new DeviceState({ deviceState = new DeviceState({
db,
config, config,
eventTracker: eventTracker as any, eventTracker: eventTracker as any,
logger: logger as any, logger: logger as any,
@ -252,7 +249,6 @@ describe('deviceState', () => {
}); });
(deviceState as any).deviceConfig.configBackend = new RPiConfigBackend(); (deviceState as any).deviceConfig.configBackend = new RPiConfigBackend();
await db.init();
await config.init(); await config.init();
}); });

View File

@ -1,23 +1,21 @@
import { fs } from 'mz'; import { fs } from 'mz';
import { Server } from 'net'; import { Server } from 'net';
import { SinonSpy, spy, stub, SinonStub } from 'sinon'; import { SinonSpy, SinonStub, spy, stub } from 'sinon';
import ApiBinder from '../src/api-binder';
import Config from '../src/config';
import DeviceState from '../src/device-state';
import Log from '../src/lib/supervisor-console';
import chai = require('./lib/chai-config'); import chai = require('./lib/chai-config');
import balenaAPI = require('./lib/mocked-balena-api'); import balenaAPI = require('./lib/mocked-balena-api');
import prepare = require('./lib/prepare'); import prepare = require('./lib/prepare');
import ApiBinder from '../src/api-binder';
import Config from '../src/config';
import Log from '../src/lib/supervisor-console';
import DB from '../src/db';
import DeviceState from '../src/device-state';
const { expect } = chai; const { expect } = chai;
const initModels = async (obj: Dictionary<any>, filename: string) => { const initModels = async (obj: Dictionary<any>, filename: string) => {
prepare(); await prepare();
obj.db = new DB(); obj.config = new Config({ configPath: filename });
obj.config = new Config({ db: obj.db, configPath: filename });
obj.eventTracker = { obj.eventTracker = {
track: stub().callsFake((ev, props) => console.log(ev, props)), track: stub().callsFake((ev, props) => console.log(ev, props)),
@ -30,14 +28,12 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
} as any; } as any;
obj.apiBinder = new ApiBinder({ obj.apiBinder = new ApiBinder({
db: obj.db,
config: obj.config, config: obj.config,
logger: obj.logger, logger: obj.logger,
eventTracker: obj.eventTracker, eventTracker: obj.eventTracker,
}); });
obj.deviceState = new DeviceState({ obj.deviceState = new DeviceState({
db: obj.db,
config: obj.config, config: obj.config,
eventTracker: obj.eventTracker, eventTracker: obj.eventTracker,
logger: obj.logger, logger: obj.logger,
@ -46,7 +42,6 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
obj.apiBinder.setDeviceState(obj.deviceState); obj.apiBinder.setDeviceState(obj.deviceState);
await obj.db.init();
await obj.config.init(); await obj.config.init();
await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning
}; };

View File

@ -14,9 +14,8 @@ const extlinuxBackend = new ExtlinuxConfigBackend();
const rpiConfigBackend = new RPiConfigBackend(); const rpiConfigBackend = new RPiConfigBackend();
describe('DeviceConfig', function () { describe('DeviceConfig', function () {
before(function () { before(async function () {
prepare(); await prepare();
this.fakeDB = {};
this.fakeConfig = { this.fakeConfig = {
get(key: string) { get(key: string) {
return Promise.try(function () { return Promise.try(function () {
@ -33,7 +32,6 @@ describe('DeviceConfig', function () {
}; };
return (this.deviceConfig = new DeviceConfig({ return (this.deviceConfig = new DeviceConfig({
logger: this.fakeLogger, logger: this.fakeLogger,
db: this.fakeDB,
config: this.fakeConfig, config: this.fakeConfig,
})); }));
}); });
@ -411,7 +409,6 @@ APPEND \${cbootargs} \${resin_kernel_root} ro rootwait isolcpus=2\n\
}; };
this.upboardConfig = new DeviceConfig({ this.upboardConfig = new DeviceConfig({
logger: this.fakeLogger, logger: this.fakeLogger,
db: this.fakeDB,
config: fakeConfig as any, config: fakeConfig as any,
}); });

View File

@ -3,7 +3,6 @@ import * as _ from 'lodash';
import { stub } from 'sinon'; import { stub } from 'sinon';
import Config from '../src/config'; import Config from '../src/config';
import DB from '../src/db';
import Network from '../src/compose/network'; import Network from '../src/compose/network';
@ -126,10 +125,9 @@ const dependentDBFormat = {
}; };
describe('ApplicationManager', function () { describe('ApplicationManager', function () {
before(function () { before(async function () {
prepare(); await prepare();
this.db = new DB(); this.config = new Config();
this.config = new Config({ db: this.db });
const eventTracker = new EventTracker(); const eventTracker = new EventTracker();
this.logger = { this.logger = {
clearOutOfDateDBLogs: () => { clearOutOfDateDBLogs: () => {
@ -137,7 +135,6 @@ describe('ApplicationManager', function () {
}, },
} as any; } as any;
this.deviceState = new DeviceState({ this.deviceState = new DeviceState({
db: this.db,
config: this.config, config: this.config,
eventTracker, eventTracker,
logger: this.logger, logger: this.logger,
@ -229,10 +226,8 @@ describe('ApplicationManager', function () {
return targetCloned; return targetCloned;
}); });
}; };
return this.db.init().then(() => {
return this.config.init(); return this.config.init();
}); });
});
beforeEach( beforeEach(
() => () =>

View File

@ -2,11 +2,9 @@ import * as assert from 'assert';
import { expect } from 'chai'; import { expect } from 'chai';
import * as Docker from 'dockerode'; import * as Docker from 'dockerode';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as tmp from 'tmp';
import Config from '../src/config'; import Config from '../src/config';
import DB from '../src/db'; import * as db from '../src/db';
import log from '../src/lib/supervisor-console';
import LocalModeManager, { import LocalModeManager, {
EngineSnapshot, EngineSnapshot,
EngineSnapshotRecord, EngineSnapshotRecord,
@ -15,8 +13,6 @@ import Logger from '../src/logger';
import ShortStackError from './lib/errors'; import ShortStackError from './lib/errors';
describe('LocalModeManager', () => { describe('LocalModeManager', () => {
let dbFile: tmp.FileResult;
let db: DB;
let localMode: LocalModeManager; let localMode: LocalModeManager;
let dockerStub: sinon.SinonStubbedInstance<Docker>; let dockerStub: sinon.SinonStubbedInstance<Docker>;
@ -35,10 +31,7 @@ describe('LocalModeManager', () => {
}); });
before(async () => { before(async () => {
dbFile = tmp.fileSync(); await db.initialized;
log.debug(`Test db: ${dbFile.name}`);
db = new DB({ databasePath: dbFile.name });
await db.init();
dockerStub = sinon.createStubInstance(Docker); dockerStub = sinon.createStubInstance(Docker);
const configStub = (sinon.createStubInstance(Config) as unknown) as Config; const configStub = (sinon.createStubInstance(Config) as unknown) as Config;
@ -48,7 +41,6 @@ describe('LocalModeManager', () => {
configStub, configStub,
(dockerStub as unknown) as Docker, (dockerStub as unknown) as Docker,
loggerStub, loggerStub,
db,
supervisorContainerId, supervisorContainerId,
); );
}); });
@ -441,6 +433,5 @@ describe('LocalModeManager', () => {
after(async () => { after(async () => {
sinon.restore(); sinon.restore();
dbFile.removeCallback();
}); });
}); });

View File

@ -1,12 +1,3 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import * as express from 'express'; import * as express from 'express';
import * as _ from 'lodash'; import * as _ from 'lodash';

View File

@ -3,17 +3,17 @@ import { fs } from 'mz';
import { stub } from 'sinon'; import { stub } from 'sinon';
import { ApplicationManager } from '../../src/application-manager'; import { ApplicationManager } from '../../src/application-manager';
import { Images } from '../../src/compose/images';
import { NetworkManager } from '../../src/compose/network-manager';
import { ServiceManager } from '../../src/compose/service-manager';
import { VolumeManager } from '../../src/compose/volume-manager';
import Config from '../../src/config'; import Config from '../../src/config';
import Database from '../../src/db'; import * as db from '../../src/db';
import { createV1Api } from '../../src/device-api/v1'; import { createV1Api } from '../../src/device-api/v1';
import { createV2Api } from '../../src/device-api/v2'; import { createV2Api } from '../../src/device-api/v2';
import DeviceState from '../../src/device-state'; import DeviceState from '../../src/device-state';
import EventTracker from '../../src/event-tracker'; import EventTracker from '../../src/event-tracker';
import SupervisorAPI from '../../src/supervisor-api'; import SupervisorAPI from '../../src/supervisor-api';
import { Images } from '../../src/compose/images';
import { ServiceManager } from '../../src/compose/service-manager';
import { NetworkManager } from '../../src/compose/network-manager';
import { VolumeManager } from '../../src/compose/volume-manager';
const DB_PATH = './test/data/supervisor-api.sqlite'; const DB_PATH = './test/data/supervisor-api.sqlite';
// Holds all values used for stubbing // Holds all values used for stubbing
@ -67,12 +67,11 @@ const STUBBED_VALUES = {
async function create(): Promise<SupervisorAPI> { async function create(): Promise<SupervisorAPI> {
// Get SupervisorAPI construct options // Get SupervisorAPI construct options
const { db, config, eventTracker, deviceState } = await createAPIOpts(); const { config, eventTracker, deviceState } = await createAPIOpts();
// Stub functions // Stub functions
setupStubs(); setupStubs();
// Create ApplicationManager // Create ApplicationManager
const appManager = new ApplicationManager({ const appManager = new ApplicationManager({
db,
config, config,
eventTracker, eventTracker,
logger: null, logger: null,
@ -102,27 +101,21 @@ async function cleanUp(): Promise<void> {
} }
async function createAPIOpts(): Promise<SupervisorAPIOpts> { async function createAPIOpts(): Promise<SupervisorAPIOpts> {
// Create database await db.initialized;
const db = new Database({
databasePath: DB_PATH,
});
await db.init();
// Create config // Create config
const mockedConfig = new Config({ db }); const mockedConfig = new Config();
// Initialize and set values for mocked Config // Initialize and set values for mocked Config
await initConfig(mockedConfig); await initConfig(mockedConfig);
// Create EventTracker // Create EventTracker
const tracker = new EventTracker(); const tracker = new EventTracker();
// Create deviceState // Create deviceState
const deviceState = new DeviceState({ const deviceState = new DeviceState({
db,
config: mockedConfig, config: mockedConfig,
eventTracker: tracker, eventTracker: tracker,
logger: null as any, logger: null as any,
apiBinder: null as any, apiBinder: null as any,
}); });
return { return {
db,
config: mockedConfig, config: mockedConfig,
eventTracker: tracker, eventTracker: tracker,
deviceState, deviceState,
@ -172,7 +165,6 @@ function restoreStubs() {
} }
interface SupervisorAPIOpts { interface SupervisorAPIOpts {
db: Database;
config: Config; config: Config;
eventTracker: EventTracker; eventTracker: EventTracker;
deviceState: DeviceState; deviceState: DeviceState;

View File

@ -1,18 +1,23 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import * as fs from 'fs'; import * as fs from 'fs';
import * as db from '../../src/db';
export = function () { export = async function () {
try { await db.initialized;
fs.unlinkSync(process.env.DATABASE_PATH!); await db.transaction(async (trx) => {
} catch (e) { const result = await trx.raw(`
/* ignore /*/ SELECT name, sql
FROM sqlite_master
WHERE type='table'`);
for (const r of result) {
// We don't run the migrations again
if (r.name !== 'knex_migrations') {
await trx.raw(`DELETE FROM ${r.name}`);
} }
}
// The supervisor expects this value to already have
// been pre-populated
await trx('deviceConfig').insert({ targetValues: '{}' });
});
try { try {
fs.unlinkSync(process.env.DATABASE_PATH_2!); fs.unlinkSync(process.env.DATABASE_PATH_2!);

View File

@ -2,8 +2,8 @@
// TODO: Upstream types to the repo // TODO: Upstream types to the repo
declare module 'balena-register-device' { declare module 'balena-register-device' {
import TypedError = require('typed-error');
import { Response } from 'request'; import { Response } from 'request';
import TypedError = require('typed-error');
function factory({ function factory({
request, request,
@ -13,7 +13,7 @@ declare module 'balena-register-device' {
}; };
factory.ApiError = class ApiError extends TypedError { factory.ApiError = class ApiError extends TypedError {
response: Response; public response: Response;
}; };
export = factory; export = factory;
} }