mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-23 23:42:29 +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 { 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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user