mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 13:47:54 +00:00
Move supervisor-api.ts to device-api/index.ts
Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
parent
b7c497cc65
commit
5af146ec4e
@ -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;
|
@ -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;
|
@ -6,7 +6,7 @@ import { intialiseContractRequirements } from './lib/contracts';
|
||||
import { normaliseLegacyDatabase } from './lib/legacy';
|
||||
import * as osRelease from './lib/os-release';
|
||||
import * as logger from './logger';
|
||||
import SupervisorAPI from './supervisor-api';
|
||||
import SupervisorAPI from './device-api';
|
||||
import log from './lib/supervisor-console';
|
||||
import version = require('./lib/supervisor-version');
|
||||
import * as avahi from './lib/avahi';
|
||||
|
@ -6,7 +6,7 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
|
||||
import * as apiBinder from '~/src/api-binder';
|
||||
import * as deviceState from '~/src/device-state';
|
||||
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 db from '~/src/db';
|
||||
import { cloudApiKey } from '~/lib/api-keys';
|
||||
|
@ -20,7 +20,7 @@ import mockedAPI = require('~/test-lib/mocked-device-api');
|
||||
import sampleResponses = require('~/test-data/device-api-responses.json');
|
||||
import * as config from '~/src/config';
|
||||
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 deviceState from '~/src/device-state';
|
||||
import * as apiKeys from '~/lib/api-keys';
|
||||
|
@ -7,7 +7,7 @@ import sampleResponses = require('~/test-data/device-api-responses.json');
|
||||
import mockedAPI = require('~/test-lib/mocked-device-api');
|
||||
import * as apiBinder from '~/src/api-binder';
|
||||
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 images from '~/src/compose/images';
|
||||
import * as apiKeys from '~/lib/api-keys';
|
||||
|
@ -12,7 +12,7 @@ import * as db from '~/src/db';
|
||||
import { createV1Api } from '~/src/device-api/v1';
|
||||
import { createV2Api } from '~/src/device-api/v2';
|
||||
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 { Image } from '~/src/compose/images';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user