mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-24 10:55:04 +00:00
The supervisor uses the following pattern for async module initialization ```typescript // module.ts export const initialised = (async () => { // do some async initialization })(); // somewhere else import * as module from 'module'; async function setup() { await module.initialise; } ``` The above pattern means that whenever the module is imported, the initialisation procedure will be ran, which is an anti-pattern. This converts any instance of this pattern into a function ```typescript export const initialised = _.once(async () => { // do some async initialization }); ``` And anywhere else on the code it replaces the call with a ```typescript await module.initialised(); ``` Change-type: patch
176 lines
4.5 KiB
TypeScript
176 lines
4.5 KiB
TypeScript
import { NextFunction, Request, Response } from 'express';
|
|
import * as express from 'express';
|
|
import { Server } from 'http';
|
|
import * as _ from 'lodash';
|
|
import * as morgan from 'morgan';
|
|
|
|
import * as eventTracker from './event-tracker';
|
|
import blink = require('./lib/blink');
|
|
|
|
import log from './lib/supervisor-console';
|
|
import * as apiKeys from './lib/api-keys';
|
|
import * as deviceState from './device-state';
|
|
import { UpdatesLockedError } from './lib/errors';
|
|
|
|
const expressLogger = morgan(
|
|
(tokens, req, res) =>
|
|
[
|
|
tokens.method(req, res),
|
|
req.path,
|
|
tokens.status(req, res),
|
|
'-',
|
|
tokens['response-time'](req, res),
|
|
'ms',
|
|
].join(' '),
|
|
{
|
|
stream: { write: (d) => log.api(d.toString().trimRight()) },
|
|
},
|
|
);
|
|
|
|
interface SupervisorAPIConstructOpts {
|
|
routers: express.Router[];
|
|
healthchecks: Array<() => Promise<boolean>>;
|
|
}
|
|
|
|
interface SupervisorAPIStopOpts {
|
|
errored: boolean;
|
|
}
|
|
|
|
export class SupervisorAPI {
|
|
private routers: express.Router[];
|
|
private healthchecks: Array<() => Promise<boolean>>;
|
|
|
|
private api = express();
|
|
private server: Server | null = null;
|
|
|
|
public constructor({ routers, healthchecks }: SupervisorAPIConstructOpts) {
|
|
this.routers = routers;
|
|
this.healthchecks = healthchecks;
|
|
|
|
this.api.disable('x-powered-by');
|
|
this.api.use(expressLogger);
|
|
|
|
this.api.get('/v1/healthy', async (_req, res) => {
|
|
try {
|
|
const healths = await Promise.all(this.healthchecks.map((fn) => fn()));
|
|
if (!_.every(healths)) {
|
|
log.error('Healthcheck failed');
|
|
return res.status(500).send('Unhealthy');
|
|
}
|
|
return res.sendStatus(200);
|
|
} catch (_e) {
|
|
log.error('Healthcheck failed');
|
|
return res.status(500).send('Unhealthy');
|
|
}
|
|
});
|
|
|
|
this.api.get('/ping', (_req, res) => res.send('OK'));
|
|
|
|
this.api.use(apiKeys.authMiddleware);
|
|
|
|
this.api.post('/v1/blink', (_req, res) => {
|
|
eventTracker.track('Device blink');
|
|
blink.pattern.start();
|
|
setTimeout(blink.pattern.stop, 15000);
|
|
return res.sendStatus(200);
|
|
});
|
|
|
|
// Expires the supervisor's API key and generates a new one.
|
|
// It also communicates the new key to the balena API.
|
|
this.api.post(
|
|
'/v1/regenerate-api-key',
|
|
async (req: apiKeys.AuthorizedRequest, res) => {
|
|
await deviceState.initialized();
|
|
await apiKeys.initialized();
|
|
|
|
// check if we're updating the cloud API key
|
|
const updateCloudKey = req.auth.apiKey === apiKeys.cloudApiKey;
|
|
|
|
// regenerate the key...
|
|
const newKey = await apiKeys.refreshKey(req.auth.apiKey);
|
|
|
|
// if we need to update the cloud API with our new key
|
|
if (updateCloudKey) {
|
|
// report the new key to the cloud API
|
|
deviceState.reportCurrentState({
|
|
api_secret: apiKeys.cloudApiKey,
|
|
});
|
|
}
|
|
|
|
// return the value of the new key to the caller
|
|
res.status(200).send(newKey);
|
|
},
|
|
);
|
|
|
|
// And assign all external routers
|
|
for (const router of this.routers) {
|
|
this.api.use(router);
|
|
}
|
|
|
|
// Error handling.
|
|
const messageFromError = (err?: Error | string | null): string => {
|
|
let message = 'Unknown error';
|
|
if (err != null) {
|
|
if (_.isError(err) && err.message != null) {
|
|
message = err.message;
|
|
} else {
|
|
message = err as string;
|
|
}
|
|
}
|
|
return message;
|
|
};
|
|
|
|
this.api.use(
|
|
(err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
if (res.headersSent) {
|
|
// Error happens while we are writing the response - default handler closes the connection.
|
|
next(err);
|
|
return;
|
|
}
|
|
|
|
// Return 423 Locked when locks as set
|
|
const code = err instanceof UpdatesLockedError ? 423 : 503;
|
|
if (code !== 423) {
|
|
log.error(`Error on ${req.method} ${req.path}: `, err);
|
|
}
|
|
|
|
res.status(code).send({
|
|
status: 'failed',
|
|
message: messageFromError(err),
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
public async listen(port: number, apiTimeout: number): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
this.server = this.api.listen(port, () => {
|
|
log.info(`Supervisor API successfully started on port ${port}`);
|
|
if (this.server) {
|
|
this.server.timeout = apiTimeout;
|
|
}
|
|
return resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
public async stop(options?: SupervisorAPIStopOpts): Promise<void> {
|
|
if (this.server != null) {
|
|
return new Promise((resolve, reject) => {
|
|
this.server?.close((err: Error) => {
|
|
if (err) {
|
|
log.error('Failed to stop Supervisor API');
|
|
return reject(err);
|
|
}
|
|
options?.errored
|
|
? log.error('Stopped Supervisor API')
|
|
: log.info('Stopped Supervisor API');
|
|
return resolve();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export default SupervisorAPI;
|