Add more typescript conversions and export utilities from existing

Change-type: patch
Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
Cameron Diver 2019-06-21 12:21:52 +01:00
parent 4522d1ad86
commit 20a83e8e0a
11 changed files with 206 additions and 138 deletions

View File

@ -9,7 +9,7 @@ import { PinejsClientRequest, StatusError } from 'pinejs-client-request';
import * as deviceRegister from 'resin-register-device';
import * as url from 'url';
import Config from './config';
import Config, { ConfigType } from './config';
import Database from './db';
import DeviceConfig from './device-config';
import { EventTracker } from './event-tracker';
@ -25,10 +25,10 @@ import { request, requestOpts } from './lib/request';
import { writeLock } from './lib/update-lock';
import { DeviceApplicationState } from './types/state';
import { SchemaReturn as ConfigSchemaType } from './config/schema-type';
import log from './lib/supervisor-console';
import DeviceState = require('./device-state');
const REPORT_SUCCESS_DELAY = 1000;
const MAX_REPORT_RETRY_DELAY = 60000;
@ -42,11 +42,7 @@ export interface APIBinderConstructOpts {
config: Config;
// FIXME: Remove this
db: Database;
// TODO: Typings
deviceState: {
deviceConfig: DeviceConfig;
[key: string]: any;
};
deviceState: DeviceState;
eventTracker: EventTracker;
}
@ -67,7 +63,7 @@ interface DeviceTag {
value: string;
}
type KeyExchangeOpts = ConfigSchemaType<'provisioningOptions'>;
type KeyExchangeOpts = ConfigType<'provisioningOptions'>;
export class APIBinder {
public router: express.Router;
@ -79,7 +75,7 @@ export class APIBinder {
};
private eventTracker: EventTracker;
private balenaApi: PinejsClientRequest | null = null;
public balenaApi: PinejsClientRequest | null = null;
private cachedBalenaApi: PinejsClientRequest | null = null;
private lastReportedState: DeviceApplicationState = {
local: {},
@ -972,3 +968,5 @@ export class APIBinder {
return router;
}
}
export default APIBinder;

View File

@ -1,15 +0,0 @@
do ->
# Make NodeJS RFC 3484 compliant for properly handling IPv6
# See: https://github.com/nodejs/node/pull/14731
# https://github.com/nodejs/node/pull/17793
dns = require('dns')
{ lookup } = dns
dns.lookup = (name, opts, cb) ->
if typeof cb isnt 'function'
return lookup(name, { verbatim: true }, opts)
return lookup(name, Object.assign({ verbatim: true }, opts), cb)
Supervisor = require './supervisor'
supervisor = new Supervisor()
supervisor.init()

21
src/app.ts Normal file
View File

@ -0,0 +1,21 @@
// This was originally wrapped in a do block in
// coffeescript, and it's not clear now why that was the
// case, so I'm going to maintain that behaviour
(() => {
// Make NodeJS RFC 3484 compliant for properly handling IPv6
// See: https://github.com/nodejs/node/pull/14731
// https://github.com/nodejs/node/pull/17793
const dns = require('dns');
const { lookup } = dns;
dns.lookup = (name: string, opts: any, cb: (err?: Error) => void) => {
if (typeof cb !== 'function') {
return lookup(name, { verbatim: true }, opts);
}
return lookup(name, Object.assign({ verbatim: true }, opts), cb);
};
})();
import Supervisor from './supervisor';
const supervisor = new Supervisor();
supervisor.init();

View File

@ -45,6 +45,8 @@ export class ApplicationManager extends EventEmitter {
public db: DB;
public images: Images;
public proxyvisor: any;
public getCurrentApp(appId: number): Bluebird<Application | null>;
// TODO: This actually returns an object, but we don't need the values just yet

View File

@ -23,7 +23,7 @@ import {
interface ConfigOpts {
db: DB;
configPath: string;
configPath?: string;
}
type ConfigMap<T extends SchemaTypeKey> = { [key in T]: SchemaReturn<key> };
@ -31,6 +31,10 @@ type ConfigChangeMap<T extends SchemaTypeKey> = {
[key in T]?: SchemaReturn<key>
};
// Export this type renamed, for storing config keys
export type ConfigKey = SchemaTypeKey;
export type ConfigType<T extends ConfigKey> = SchemaReturn<T>;
interface ConfigEvents {
change: ConfigChangeMap<SchemaTypeKey>;
}

31
src/device-state.d.ts vendored Normal file
View File

@ -0,0 +1,31 @@
import { EventEmitter } from 'events';
import { Router } from 'express';
import ApplicationManager from './application-manager';
import Config from './config';
import Database from './db';
import DeviceConfig from './device-config';
import EventTracker from './event-tracker';
import Logger from './logger';
// This is a very incomplete definition of the device state
// class, which should be rewritten in typescript soon
class DeviceState extends EventEmitter {
public applications: ApplicationManager;
public router: Router;
public deviceConfig: DeviceConfig;
public constructor(args: {
config: Config;
db: Database;
eventTracker: EventTracker;
logger: Logger;
});
public healthcheck(): Promise<void>;
public normaliseLegacy(client: PinejsClientRequest): Promise<void>;
public async init();
}
export = DeviceState;

View File

@ -5,16 +5,17 @@ import * as memoizee from 'memoizee';
import Mixpanel = require('mixpanel');
import { ConfigType } from './config';
import log from './lib/supervisor-console';
import supervisorVersion = require('./lib/supervisor-version');
export type EventTrackProperties = Dictionary<any>;
interface InitArgs {
uuid: string;
unmanaged: boolean;
mixpanelHost: { host: string; path: string } | null;
mixpanelToken: string;
uuid: ConfigType<'uuid'>;
unmanaged: ConfigType<'unmanaged'>;
mixpanelHost: ConfigType<'mixpanelHost'>;
mixpanelToken: ConfigType<'mixpanelToken'>;
}
// The minimum amount of time to wait between sending
@ -112,3 +113,5 @@ export class EventTracker {
return _.merge({}, properties, this.defaultProperties);
}
}
export default EventTracker;

View File

@ -1,6 +1,7 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { ConfigType } from './config';
import DB from './db';
import { EventTracker } from './event-tracker';
import Docker from './lib/docker-utils';
@ -18,12 +19,13 @@ import LogMonitor from './logging/monitor';
import log from './lib/supervisor-console';
interface LoggerSetupOptions {
apiEndpoint: string;
uuid: string;
deviceApiKey: string;
unmanaged: boolean;
apiEndpoint: ConfigType<'apiEndpoint'>;
uuid: ConfigType<'uuid'>;
deviceApiKey: ConfigType<'deviceApiKey'>;
unmanaged: ConfigType<'unmanaged'>;
localMode: ConfigType<'localMode'>;
enableLogs: boolean;
localMode: boolean;
}
type LogEventObject = Dictionary<any> | null;
@ -58,7 +60,16 @@ export class Logger {
enableLogs,
localMode,
}: LoggerSetupOptions) {
this.balenaBackend = new BalenaLogBackend(apiEndpoint, uuid, deviceApiKey);
this.balenaBackend = new BalenaLogBackend(
apiEndpoint,
// This is definitely not correct, but this is indeed
// what has been happening before the conversion of
// `supervisor.coffee` to typescript. We need to fix
// the wider problem of the backend attempting to
// communicate with the api without being provisioned
uuid || '',
deviceApiKey,
);
this.localBackend = new LocalLogBackend();
this.backend = localMode ? this.localBackend : this.balenaBackend;

View File

@ -1,101 +0,0 @@
EventEmitter = require 'events'
{ EventTracker } = require './event-tracker'
{ DB } = require './db'
{ Config } = require './config'
{ APIBinder } = require './api-binder'
DeviceState = require './device-state'
{ SupervisorAPI } = require './supervisor-api'
{ Logger } = require './logger'
version = require './lib/supervisor-version'
{ log } = require './lib/supervisor-console'
constants = require './lib/constants'
startupConfigFields = [
'uuid'
'listenPort'
'apiEndpoint'
'apiSecret'
'apiTimeout'
'unmanaged'
'deviceApiKey'
'mixpanelToken'
'mixpanelHost'
'loggingEnabled'
'localMode'
'legacyAppsPresent'
]
module.exports = class Supervisor extends EventEmitter
constructor: ->
@db = new DB()
@config = new Config({ @db })
@eventTracker = new EventTracker()
@logger = new Logger({ @db, @eventTracker })
@deviceState = new DeviceState({ @config, @db, @eventTracker, @logger })
@apiBinder = new APIBinder({ @config, @db, @deviceState, @eventTracker })
# FIXME: rearchitect proxyvisor to avoid this circular dependency
# by storing current state and having the APIBinder query and report it / provision devices
@deviceState.applications.proxyvisor.bindToAPI(@apiBinder)
# We could also do without the below dependency, but it's part of a much larger refactor
@deviceState.applications.apiBinder = @apiBinder
@api = new SupervisorAPI({
@config,
@eventTracker,
routers: [
@apiBinder.router,
@deviceState.router
],
healthchecks: [
@apiBinder.healthcheck.bind(@apiBinder),
@deviceState.healthcheck.bind(@deviceState)
]
})
init: =>
log.info("Supervisor v#{version} starting up...")
@db.init()
.tap =>
@config.init() # Ensures uuid, deviceApiKey, apiSecret
.then =>
@config.getMany(startupConfigFields)
.then (conf) =>
# We can't print to the dashboard until the logger has started up,
# so we leave a trail of breadcrumbs in the logs in case runtime
# fails to get to the first dashboard logs
log.debug('Starting event tracker')
@eventTracker.init(conf)
.then =>
log.debug('Starting up api binder')
@apiBinder.initClient()
.then =>
log.debug('Starting logging infrastructure')
@logger.init({
apiEndpoint: conf.apiEndpoint,
uuid: conf.uuid,
deviceApiKey: conf.deviceApiKey,
unmanaged: conf.unmanaged,
enableLogs: conf.loggingEnabled,
localMode: conf.localMode
})
.then =>
@logger.logSystemMessage('Supervisor starting', {}, 'Supervisor start')
.then =>
if conf.legacyAppsPresent
log.info('Legacy app detected, running migration')
@deviceState.normaliseLegacy(@apiBinder.balenaApi)
.then =>
@deviceState.init()
.then =>
# initialize API
log.info('Starting API server')
@api.listen(constants.allowedInterfaces, conf.listenPort, conf.apiTimeout)
@deviceState.on('shutdown', => @api.stop())
.then =>
@apiBinder.start() # this will first try to provision if it's a new device

114
src/supervisor.ts Normal file
View File

@ -0,0 +1,114 @@
import APIBinder from './api-binder';
import Config, { ConfigKey } from './config';
import Database from './db';
import EventTracker from './event-tracker';
import Logger from './logger';
import SupervisorAPI from './supervisor-api';
import DeviceState = require('./device-state');
import constants = require('./lib/constants');
import log from './lib/supervisor-console';
import version = require('./lib/supervisor-version');
const startupConfigFields: ConfigKey[] = [
'uuid',
'listenPort',
'apiEndpoint',
'apiSecret',
'apiTimeout',
'unmanaged',
'deviceApiKey',
'mixpanelToken',
'mixpanelHost',
'loggingEnabled',
'localMode',
'legacyAppsPresent',
];
export class Supervisor {
private db: Database;
private config: Config;
private eventTracker: EventTracker;
private logger: Logger;
private deviceState: DeviceState;
private apiBinder: APIBinder;
private api: SupervisorAPI;
public constructor() {
this.db = new Database();
this.config = new Config({ db: this.db });
this.eventTracker = new EventTracker();
this.logger = new Logger({ db: this.db, eventTracker: this.eventTracker });
this.deviceState = new DeviceState({
config: this.config,
db: this.db,
eventTracker: this.eventTracker,
logger: this.logger,
});
this.apiBinder = new APIBinder({
config: this.config,
db: this.db,
deviceState: this.deviceState,
eventTracker: this.eventTracker,
});
// FIXME: rearchitect proxyvisor to avoid this circular dependency
// by storing current state and having the APIBinder query and report it / provision devices
this.deviceState.applications.proxyvisor.bindToAPI(this.apiBinder);
// We could also do without the below dependency, but it's part of a much larger refactor
this.deviceState.applications.apiBinder = this.apiBinder;
this.api = new SupervisorAPI({
config: this.config,
eventTracker: this.eventTracker,
routers: [this.apiBinder.router, this.deviceState.router],
healthchecks: [
this.apiBinder.healthcheck.bind(this.apiBinder),
this.deviceState.healthcheck.bind(this.deviceState),
],
});
}
public async init() {
log.info(`Supervisor v${version} starting up...`);
await this.db.init();
await this.config.init();
const conf = await this.config.getMany(startupConfigFields);
// We can't print to the dashboard until the logger
// has started up, so we leave a trail of breadcrumbs
// in the logs in case runtime fails to get to the
// first dashboard logs
log.debug('Starting event tracker');
await this.eventTracker.init(conf);
log.debug('Starting api binder');
await this.apiBinder.initClient();
log.debug('Starting logging infrastructure');
this.logger.init({ enableLogs: conf.loggingEnabled, ...conf });
this.logger.logSystemMessage('Supervisor starting', {}, 'Supervisor start');
if (conf.legacyAppsPresent) {
log.info('Legacy app detected, running migration');
this.deviceState.normaliseLegacy(this.apiBinder.balenaApi);
}
await this.deviceState.init();
log.info('Starting API server');
this.api.listen(
constants.allowedInterfaces,
conf.listenPort,
conf.apiTimeout,
);
this.deviceState.on('shutdown', () => this.api.stop());
await this.apiBinder.start();
}
}
export default Supervisor;

View File

@ -69,7 +69,7 @@ module.exports = function(env) {
return {
mode: env == null || !env.noOptimize ? 'production' : 'development',
devtool: 'none',
entry: './src/app.coffee',
entry: './src/app.ts',
output: {
filename: 'app.js',
path: path.resolve(__dirname, 'dist'),