Move supervisor-api.ts to device-api/index.ts

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2022-09-15 18:51:17 -07:00
parent b7c497cc65
commit 5af146ec4e
7 changed files with 180 additions and 180 deletions

View File

@ -0,0 +1,175 @@
import * as express from 'express';
import * as _ from 'lodash';
import * as morgan from 'morgan';
import * as eventTracker from '../event-tracker';
import * as deviceState from '../device-state';
import blink = require('../lib/blink');
import log from '../lib/supervisor-console';
import * as apiKeys from '../lib/api-keys';
import { UpdatesLockedError } from '../lib/errors';
import type { Server } from 'http';
import type { NextFunction, Request, Response } from 'express';
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 {
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;

View File

@ -1,175 +0,0 @@
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';
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(
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()) },
},
),
);
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 {
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;

View File

@ -6,7 +6,7 @@ import { intialiseContractRequirements } from './lib/contracts';
import { normaliseLegacyDatabase } from './lib/legacy'; import { normaliseLegacyDatabase } from './lib/legacy';
import * as osRelease from './lib/os-release'; import * as osRelease from './lib/os-release';
import * as logger from './logger'; import * as logger from './logger';
import SupervisorAPI from './supervisor-api'; import SupervisorAPI from './device-api';
import log from './lib/supervisor-console'; import log from './lib/supervisor-console';
import version = require('./lib/supervisor-version'); import version = require('./lib/supervisor-version');
import * as avahi from './lib/avahi'; import * as avahi from './lib/avahi';

View File

@ -6,7 +6,7 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
import * as apiBinder from '~/src/api-binder'; import * as apiBinder from '~/src/api-binder';
import * as deviceState from '~/src/device-state'; import * as deviceState from '~/src/device-state';
import Log from '~/lib/supervisor-console'; import Log from '~/lib/supervisor-console';
import SupervisorAPI from '~/src/supervisor-api'; import SupervisorAPI from '~/src/device-api';
import * as apiKeys from '~/lib/api-keys'; import * as apiKeys from '~/lib/api-keys';
import * as db from '~/src/db'; import * as db from '~/src/db';
import { cloudApiKey } from '~/lib/api-keys'; import { cloudApiKey } from '~/lib/api-keys';

View File

@ -20,7 +20,7 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
import sampleResponses = require('~/test-data/device-api-responses.json'); import sampleResponses = require('~/test-data/device-api-responses.json');
import * as config from '~/src/config'; import * as config from '~/src/config';
import * as logger from '~/src/logger'; import * as logger from '~/src/logger';
import SupervisorAPI from '~/src/supervisor-api'; import SupervisorAPI from '~/src/device-api';
import * as apiBinder from '~/src/api-binder'; import * as apiBinder from '~/src/api-binder';
import * as deviceState from '~/src/device-state'; import * as deviceState from '~/src/device-state';
import * as apiKeys from '~/lib/api-keys'; import * as apiKeys from '~/lib/api-keys';

View File

@ -7,7 +7,7 @@ import sampleResponses = require('~/test-data/device-api-responses.json');
import mockedAPI = require('~/test-lib/mocked-device-api'); import mockedAPI = require('~/test-lib/mocked-device-api');
import * as apiBinder from '~/src/api-binder'; import * as apiBinder from '~/src/api-binder';
import * as deviceState from '~/src/device-state'; import * as deviceState from '~/src/device-state';
import SupervisorAPI from '~/src/supervisor-api'; import SupervisorAPI from '~/src/device-api';
import * as serviceManager from '~/src/compose/service-manager'; import * as serviceManager from '~/src/compose/service-manager';
import * as images from '~/src/compose/images'; import * as images from '~/src/compose/images';
import * as apiKeys from '~/lib/api-keys'; import * as apiKeys from '~/lib/api-keys';

View File

@ -12,7 +12,7 @@ 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 * as deviceState from '~/src/device-state'; import * as deviceState from '~/src/device-state';
import SupervisorAPI from '~/src/supervisor-api'; import SupervisorAPI from '~/src/device-api';
import { Service } from '~/src/compose/service'; import { Service } from '~/src/compose/service';
import { Image } from '~/src/compose/images'; import { Image } from '~/src/compose/images';