diff --git a/src/api-binder.ts b/src/api-binder.ts index 892719e6..b08297cb 100644 --- a/src/api-binder.ts +++ b/src/api-binder.ts @@ -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; diff --git a/src/app.coffee b/src/app.coffee deleted file mode 100644 index ce502741..00000000 --- a/src/app.coffee +++ /dev/null @@ -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() diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..369c4a51 --- /dev/null +++ b/src/app.ts @@ -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(); diff --git a/src/application-manager.d.ts b/src/application-manager.d.ts index b3100a91..af286f8f 100644 --- a/src/application-manager.d.ts +++ b/src/application-manager.d.ts @@ -45,6 +45,8 @@ export class ApplicationManager extends EventEmitter { public db: DB; public images: Images; + public proxyvisor: any; + public getCurrentApp(appId: number): Bluebird; // TODO: This actually returns an object, but we don't need the values just yet diff --git a/src/config/index.ts b/src/config/index.ts index b0b3e1cf..56b3d330 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -23,7 +23,7 @@ import { interface ConfigOpts { db: DB; - configPath: string; + configPath?: string; } type ConfigMap = { [key in T]: SchemaReturn }; @@ -31,6 +31,10 @@ type ConfigChangeMap = { [key in T]?: SchemaReturn }; +// Export this type renamed, for storing config keys +export type ConfigKey = SchemaTypeKey; +export type ConfigType = SchemaReturn; + interface ConfigEvents { change: ConfigChangeMap; } diff --git a/src/device-state.d.ts b/src/device-state.d.ts new file mode 100644 index 00000000..1732855f --- /dev/null +++ b/src/device-state.d.ts @@ -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; + public normaliseLegacy(client: PinejsClientRequest): Promise; + + public async init(); +} + +export = DeviceState; diff --git a/src/event-tracker.ts b/src/event-tracker.ts index 70a43a7d..a7800c2f 100644 --- a/src/event-tracker.ts +++ b/src/event-tracker.ts @@ -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; 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; diff --git a/src/logger.ts b/src/logger.ts index c049d917..6d904f0d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -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 | 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; diff --git a/src/supervisor.coffee b/src/supervisor.coffee deleted file mode 100644 index 410a9e39..00000000 --- a/src/supervisor.coffee +++ /dev/null @@ -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 diff --git a/src/supervisor.ts b/src/supervisor.ts new file mode 100644 index 00000000..70d31b3b --- /dev/null +++ b/src/supervisor.ts @@ -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; diff --git a/webpack.config.js b/webpack.config.js index 56c347e1..d645e0b5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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'),