mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-19 19:28:59 +00:00
Unify API errors processing
With this change, we define a custom error handler as express middleware which renders 503 error with JSON response that includes status and message fields. The handler also logs the error, so the stack can be inspected in supervisor logs. It's also a point where we can report the error to analytics services. This removes a bunch of error handlers written in every request handler function. Behaviour should remain unchanged except the fact that /healthy endpoint now returns 503 in case of failure instead of 500. Change-type: patch Signed-off-by: Roman Mazur <roman@balena.io>
This commit is contained in:
parent
89d9e9d117
commit
8b4c9837fa
@ -935,7 +935,7 @@ export class APIBinder {
|
||||
router.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
|
||||
router.use(bodyParser.json({ limit: '10mb' }));
|
||||
|
||||
router.post('/v1/update', (req, res) => {
|
||||
router.post('/v1/update', (req, res, next) => {
|
||||
apiBinder.eventTracker.track('Update notification');
|
||||
if (apiBinder.readyForUpdates) {
|
||||
this.config
|
||||
@ -953,15 +953,7 @@ export class APIBinder {
|
||||
res.sendStatus(202);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
const msg =
|
||||
err.message != null
|
||||
? err.message
|
||||
: err != null
|
||||
? err
|
||||
: 'Unknown error';
|
||||
res.status(503).send(msg);
|
||||
});
|
||||
.catch(next);
|
||||
} else {
|
||||
res.sendStatus(202);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ exports.createV1Api = (router, applications) ->
|
||||
|
||||
{ eventTracker } = applications
|
||||
|
||||
router.post '/v1/restart', (req, res) ->
|
||||
router.post '/v1/restart', (req, res, next) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
eventTracker.track('Restart container (v1)', { appId })
|
||||
@ -18,10 +18,9 @@ exports.createV1Api = (router, applications) ->
|
||||
doRestart(applications, appId, force)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
.catch(next)
|
||||
|
||||
v1StopOrStart = (req, res, action) ->
|
||||
v1StopOrStart = (req, res, next, action) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
@ -47,16 +46,14 @@ exports.createV1Api = (router, applications) ->
|
||||
return service
|
||||
.then (service) ->
|
||||
res.status(200).json({ containerId: service.containerId })
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
.catch(next)
|
||||
|
||||
router.post '/v1/apps/:appId/stop', (req, res) ->
|
||||
v1StopOrStart(req, res, 'stop')
|
||||
createV1StopOrStartHandler = (action) -> _.partial(v1StopOrStart, _, _, _, action)
|
||||
|
||||
router.post '/v1/apps/:appId/start', (req, res) ->
|
||||
v1StopOrStart(req, res, 'start')
|
||||
router.post('/v1/apps/:appId/stop', createV1StopOrStartHandler('stop'))
|
||||
router.post('/v1/apps/:appId/start', createV1StopOrStartHandler('start'))
|
||||
|
||||
router.get '/v1/apps/:appId', (req, res) ->
|
||||
router.get '/v1/apps/:appId', (req, res, next) ->
|
||||
appId = checkInt(req.params.appId)
|
||||
eventTracker.track('GET app (v1)', { appId })
|
||||
if !appId?
|
||||
@ -82,10 +79,9 @@ exports.createV1Api = (router, applications) ->
|
||||
appToSend.commit = status.commit
|
||||
res.json(appToSend)
|
||||
)
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
.catch(next)
|
||||
|
||||
router.post '/v1/purge', (req, res) ->
|
||||
router.post '/v1/purge', (req, res, next) ->
|
||||
appId = checkInt(req.body.appId)
|
||||
force = checkTruthy(req.body.force)
|
||||
if !appId?
|
||||
@ -94,6 +90,4 @@ exports.createV1Api = (router, applications) ->
|
||||
doPurge(applications, appId, force)
|
||||
.then ->
|
||||
res.status(200).json(Data: 'OK', Error: '')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
.catch(next)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
|
||||
@ -22,21 +22,10 @@ import { checkInt, checkTruthy } from '../lib/validation';
|
||||
export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
const { _lockingIfNecessary, deviceState } = applications;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const handleServiceAction = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
action: any,
|
||||
): Bluebird<void> => {
|
||||
const { imageId, serviceName, force } = req.body;
|
||||
@ -85,15 +74,16 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
res.status(503).send(messageFromError(err));
|
||||
});
|
||||
.catch(next);
|
||||
});
|
||||
};
|
||||
|
||||
const createServiceActionHandler = (action: string) =>
|
||||
_.partial(handleServiceAction, _, _, _, action);
|
||||
|
||||
router.post(
|
||||
'/v2/applications/:appId/purge',
|
||||
(req: Request, res: Response) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const { force } = req.body;
|
||||
const { appId } = req.params;
|
||||
|
||||
@ -101,45 +91,28 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
.then(() => {
|
||||
res.status(200).send('OK');
|
||||
})
|
||||
.catch(err => {
|
||||
let message;
|
||||
if (err != null) {
|
||||
message = err.message;
|
||||
if (message == null) {
|
||||
message = err;
|
||||
}
|
||||
} else {
|
||||
message = 'Unknown error';
|
||||
}
|
||||
res.status(503).send(message);
|
||||
});
|
||||
.catch(next);
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/v2/applications/:appId/restart-service',
|
||||
(req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'restart');
|
||||
},
|
||||
createServiceActionHandler('restart'),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/v2/applications/:appId/stop-service',
|
||||
(req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'stop');
|
||||
},
|
||||
createServiceActionHandler('stop'),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/v2/applications/:appId/start-service',
|
||||
(req: Request, res: Response) => {
|
||||
return handleServiceAction(req, res, 'start');
|
||||
},
|
||||
createServiceActionHandler('start'),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/v2/applications/:appId/restart',
|
||||
(req: Request, res: Response) => {
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const { force } = req.body;
|
||||
const { appId } = req.params;
|
||||
|
||||
@ -147,192 +120,181 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
.then(() => {
|
||||
res.status(200).send('OK');
|
||||
})
|
||||
.catch(err => {
|
||||
res.status(503).send(messageFromError(err));
|
||||
});
|
||||
.catch(next);
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: Support dependent applications when this feature is complete
|
||||
router.get('/v2/applications/state', async (_req: Request, res: Response) => {
|
||||
// It's kinda hacky to access the services and db via the application manager
|
||||
// maybe refactor this code
|
||||
Bluebird.join(
|
||||
applications.services.getStatus(),
|
||||
applications.images.getStatus(),
|
||||
applications.db.models('app').select(['appId', 'commit', 'name']),
|
||||
(
|
||||
services,
|
||||
images,
|
||||
apps: Array<{ appId: string; commit: string; name: string }>,
|
||||
) => {
|
||||
// Create an object which is keyed my application name
|
||||
const response: {
|
||||
[appName: string]: {
|
||||
appId: number;
|
||||
commit: string;
|
||||
services: {
|
||||
[serviceName: string]: {
|
||||
status: string;
|
||||
releaseId: number;
|
||||
downloadProgress: number | null;
|
||||
router.get(
|
||||
'/v2/applications/state',
|
||||
async (_req: Request, res: Response, next: NextFunction) => {
|
||||
// It's kinda hacky to access the services and db via the application manager
|
||||
// maybe refactor this code
|
||||
Bluebird.join(
|
||||
applications.services.getStatus(),
|
||||
applications.images.getStatus(),
|
||||
applications.db.models('app').select(['appId', 'commit', 'name']),
|
||||
(
|
||||
services,
|
||||
images,
|
||||
apps: Array<{ appId: string; commit: string; name: string }>,
|
||||
) => {
|
||||
// Create an object which is keyed my application name
|
||||
const response: {
|
||||
[appName: string]: {
|
||||
appId: number;
|
||||
commit: string;
|
||||
services: {
|
||||
[serviceName: string]: {
|
||||
status: string;
|
||||
releaseId: number;
|
||||
downloadProgress: number | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} = {};
|
||||
} = {};
|
||||
|
||||
const appNameById: { [id: number]: string } = {};
|
||||
const appNameById: { [id: number]: string } = {};
|
||||
|
||||
apps.forEach(app => {
|
||||
const appId = parseInt(app.appId, 10);
|
||||
response[app.name] = {
|
||||
appId,
|
||||
commit: app.commit,
|
||||
services: {},
|
||||
};
|
||||
apps.forEach(app => {
|
||||
const appId = parseInt(app.appId, 10);
|
||||
response[app.name] = {
|
||||
appId,
|
||||
commit: app.commit,
|
||||
services: {},
|
||||
};
|
||||
|
||||
appNameById[appId] = app.name;
|
||||
});
|
||||
|
||||
images.forEach(img => {
|
||||
const appName = appNameById[img.appId];
|
||||
if (appName == null) {
|
||||
log.warn(
|
||||
`Image found for unknown application!\nImage: ${JSON.stringify(
|
||||
img,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const svc = _.find(services, (service: Service) => {
|
||||
return service.imageId === img.imageId;
|
||||
appNameById[appId] = app.name;
|
||||
});
|
||||
|
||||
let status: string;
|
||||
if (svc == null) {
|
||||
status = img.status;
|
||||
} else {
|
||||
status = svc.status || img.status;
|
||||
}
|
||||
response[appName].services[img.serviceName] = {
|
||||
status,
|
||||
releaseId: img.releaseId,
|
||||
downloadProgress: img.downloadProgress || null,
|
||||
};
|
||||
});
|
||||
images.forEach(img => {
|
||||
const appName = appNameById[img.appId];
|
||||
if (appName == null) {
|
||||
log.warn(
|
||||
`Image found for unknown application!\nImage: ${JSON.stringify(
|
||||
img,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
const svc = _.find(services, (service: Service) => {
|
||||
return service.imageId === img.imageId;
|
||||
});
|
||||
|
||||
let status: string;
|
||||
if (svc == null) {
|
||||
status = img.status;
|
||||
} else {
|
||||
status = svc.status || img.status;
|
||||
}
|
||||
response[appName].services[img.serviceName] = {
|
||||
status,
|
||||
releaseId: img.releaseId,
|
||||
downloadProgress: img.downloadProgress || null,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).json(response);
|
||||
},
|
||||
).catch(next);
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/v2/applications/:appId/state',
|
||||
(_req: Request, res: Response) => {
|
||||
(_req: Request, res: Response, next: NextFunction) => {
|
||||
// Get all services and their statuses, and return it
|
||||
applications.getStatus().then(apps => {
|
||||
res.status(200).json(apps);
|
||||
});
|
||||
applications
|
||||
.getStatus()
|
||||
.then(apps => {
|
||||
res.status(200).json(apps);
|
||||
})
|
||||
.catch(next);
|
||||
},
|
||||
);
|
||||
|
||||
router.get('/v2/local/target-state', async (_req, res) => {
|
||||
try {
|
||||
const localMode = await deviceState.config.get('localMode');
|
||||
if (!localMode) {
|
||||
return res.status(400).json({
|
||||
status: 'failed',
|
||||
message: 'Target state can only be retrieved when in local mode',
|
||||
});
|
||||
}
|
||||
|
||||
const targetState = await deviceState.getTarget();
|
||||
|
||||
// We avoid using cloneDeep here, as the class
|
||||
// instances can cause a maximum call stack exceeded
|
||||
// error
|
||||
|
||||
// TODO: This should really return the config as it
|
||||
// is returned from the api, but currently that's not
|
||||
// the easiest thing due to the way they are stored and
|
||||
// retrieved from the db - when all of the application
|
||||
// manager is strongly typed, revisit this. The best
|
||||
// thing to do would be to represent the input with
|
||||
// io-ts and make sure the below conforms to it
|
||||
|
||||
const target: any = {
|
||||
local: {
|
||||
config: {},
|
||||
},
|
||||
dependent: {
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
if (targetState.local != null) {
|
||||
target.local = {
|
||||
name: targetState.local.name,
|
||||
config: _.cloneDeep(targetState.local.config),
|
||||
apps: _.mapValues(targetState.local.apps, app => ({
|
||||
appId: app.appId,
|
||||
name: app.name,
|
||||
commit: app.commit,
|
||||
releaseId: app.releaseId,
|
||||
services: _.map(app.services, s => s.toComposeObject()),
|
||||
volumes: _.mapValues(app.volumes, v => v.toComposeObject()),
|
||||
networks: _.mapValues(app.networks, n => n.toComposeObject()),
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (targetState.dependent != null) {
|
||||
target.dependent = _.cloneDeep(target.dependent);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
state: target,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(503).send({
|
||||
const localMode = await deviceState.config.get('localMode');
|
||||
if (!localMode) {
|
||||
return res.status(400).json({
|
||||
status: 'failed',
|
||||
message: messageFromError(err),
|
||||
message: 'Target state can only be retrieved when in local mode',
|
||||
});
|
||||
}
|
||||
|
||||
const targetState = await deviceState.getTarget();
|
||||
|
||||
// We avoid using cloneDeep here, as the class
|
||||
// instances can cause a maximum call stack exceeded
|
||||
// error
|
||||
|
||||
// TODO: This should really return the config as it
|
||||
// is returned from the api, but currently that's not
|
||||
// the easiest thing due to the way they are stored and
|
||||
// retrieved from the db - when all of the application
|
||||
// manager is strongly typed, revisit this. The best
|
||||
// thing to do would be to represent the input with
|
||||
// io-ts and make sure the below conforms to it
|
||||
|
||||
const target: any = {
|
||||
local: {
|
||||
config: {},
|
||||
},
|
||||
dependent: {
|
||||
config: {},
|
||||
},
|
||||
};
|
||||
if (targetState.local != null) {
|
||||
target.local = {
|
||||
name: targetState.local.name,
|
||||
config: _.cloneDeep(targetState.local.config),
|
||||
apps: _.mapValues(targetState.local.apps, app => ({
|
||||
appId: app.appId,
|
||||
name: app.name,
|
||||
commit: app.commit,
|
||||
releaseId: app.releaseId,
|
||||
services: _.map(app.services, s => s.toComposeObject()),
|
||||
volumes: _.mapValues(app.volumes, v => v.toComposeObject()),
|
||||
networks: _.mapValues(app.networks, n => n.toComposeObject()),
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (targetState.dependent != null) {
|
||||
target.dependent = _.cloneDeep(target.dependent);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
state: target,
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v2/local/target-state', async (req, res) => {
|
||||
// let's first ensure that we're in local mode, otherwise
|
||||
// this function should not do anything
|
||||
try {
|
||||
const localMode = await deviceState.config.get('localMode');
|
||||
if (!localMode) {
|
||||
return res.status(400).json({
|
||||
status: 'failed',
|
||||
message: 'Target state can only set when device is in local mode',
|
||||
});
|
||||
}
|
||||
|
||||
// Now attempt to set the state
|
||||
const force = req.body.force;
|
||||
const targetState = req.body;
|
||||
try {
|
||||
await deviceState.setTarget(targetState, true);
|
||||
await deviceState.triggerApplyTarget({ force });
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
message: 'OK',
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({
|
||||
status: 'failed',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const message = 'Could not apply target state: ';
|
||||
res.status(503).json({
|
||||
const localMode = await deviceState.config.get('localMode');
|
||||
if (!localMode) {
|
||||
return res.status(400).json({
|
||||
status: 'failed',
|
||||
message: message + messageFromError(e),
|
||||
message: 'Target state can only set when device is in local mode',
|
||||
});
|
||||
}
|
||||
|
||||
// Now attempt to set the state
|
||||
const force = req.body.force;
|
||||
const targetState = req.body;
|
||||
try {
|
||||
await deviceState.setTarget(targetState, true);
|
||||
await deviceState.triggerApplyTarget({ force });
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
message: 'OK',
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(400).json({
|
||||
status: 'failed',
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -340,29 +302,21 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
router.get('/v2/local/device-info', async (_req, res) => {
|
||||
// Return the device type and slug so that local mode builds can use this to
|
||||
// resolve builds
|
||||
try {
|
||||
// FIXME: We should be mounting the following file into the supervisor from the
|
||||
// start-resin-supervisor script, changed in meta-resin - but until then, hardcode it
|
||||
const data = await fs.readFile(
|
||||
'/mnt/root/resin-boot/device-type.json',
|
||||
'utf8',
|
||||
);
|
||||
const deviceInfo = JSON.parse(data);
|
||||
// FIXME: We should be mounting the following file into the supervisor from the
|
||||
// start-resin-supervisor script, changed in meta-resin - but until then, hardcode it
|
||||
const data = await fs.readFile(
|
||||
'/mnt/root/resin-boot/device-type.json',
|
||||
'utf8',
|
||||
);
|
||||
const deviceInfo = JSON.parse(data);
|
||||
|
||||
return res.status(200).json({
|
||||
status: 'success',
|
||||
info: {
|
||||
arch: deviceInfo.arch,
|
||||
deviceType: deviceInfo.slug,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const message = 'Could not fetch device information: ';
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: message + messageFromError(e),
|
||||
});
|
||||
}
|
||||
return res.status(200).json({
|
||||
status: 'success',
|
||||
info: {
|
||||
arch: deviceInfo.arch,
|
||||
deviceType: deviceInfo.slug,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v2/local/logs', async (_req, res) => {
|
||||
@ -392,39 +346,29 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
});
|
||||
|
||||
router.get('/v2/containerId', async (req, res) => {
|
||||
try {
|
||||
const services = await applications.services.getAll();
|
||||
const services = await applications.services.getAll();
|
||||
|
||||
if (req.query.serviceName != null || req.query.service != null) {
|
||||
const serviceName = req.query.serviceName || req.query.service;
|
||||
const service = _.find(
|
||||
services,
|
||||
svc => svc.serviceName === serviceName,
|
||||
);
|
||||
if (service != null) {
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
containerId: service.containerId,
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: 'Could not find service with that name',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (req.query.serviceName != null || req.query.service != null) {
|
||||
const serviceName = req.query.serviceName || req.query.service;
|
||||
const service = _.find(services, svc => svc.serviceName === serviceName);
|
||||
if (service != null) {
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
services: _(services)
|
||||
.keyBy('serviceName')
|
||||
.mapValues('containerId')
|
||||
.value(),
|
||||
containerId: service.containerId,
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: 'Could not find service with that name',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: messageFromError(e),
|
||||
} else {
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
services: _(services)
|
||||
.keyBy('serviceName')
|
||||
.mapValues('containerId')
|
||||
.value(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -481,85 +425,51 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
|
||||
});
|
||||
|
||||
router.get('/v2/device/name', async (_req, res) => {
|
||||
try {
|
||||
const deviceName = await applications.config.get('name');
|
||||
res.json({
|
||||
status: 'success',
|
||||
deviceName,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: messageFromError(e),
|
||||
});
|
||||
}
|
||||
const deviceName = await applications.config.get('name');
|
||||
res.json({
|
||||
status: 'success',
|
||||
deviceName,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v2/device/tags', async (_req, res) => {
|
||||
try {
|
||||
const tags = await applications.apiBinder.fetchDeviceTags();
|
||||
return res.json({
|
||||
status: 'success',
|
||||
tags,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: messageFromError(e),
|
||||
});
|
||||
}
|
||||
const tags = await applications.apiBinder.fetchDeviceTags();
|
||||
return res.json({
|
||||
status: 'success',
|
||||
tags,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/v2/cleanup-volumes', async (_req, res) => {
|
||||
try {
|
||||
const targetState = await applications.getTargetApps();
|
||||
const referencedVolumes: string[] = [];
|
||||
_.each(targetState, app => {
|
||||
_.each(app.volumes, vol => {
|
||||
referencedVolumes.push(
|
||||
Volume.generateDockerName(vol.appId, vol.name),
|
||||
);
|
||||
});
|
||||
const targetState = await applications.getTargetApps();
|
||||
const referencedVolumes: string[] = [];
|
||||
_.each(targetState, app => {
|
||||
_.each(app.volumes, vol => {
|
||||
referencedVolumes.push(Volume.generateDockerName(vol.appId, vol.name));
|
||||
});
|
||||
await applications.volumes.removeOrphanedVolumes(referencedVolumes);
|
||||
res.json({
|
||||
status: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(503).json({
|
||||
status: 'failed',
|
||||
message: messageFromError(e),
|
||||
});
|
||||
}
|
||||
});
|
||||
await applications.volumes.removeOrphanedVolumes(referencedVolumes);
|
||||
res.json({
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/v2/journal-logs', (req, res) => {
|
||||
try {
|
||||
const all = checkTruthy(req.body.all) || false;
|
||||
const follow = checkTruthy(req.body.follow) || false;
|
||||
const count = checkInt(req.body.count, { positive: true }) || undefined;
|
||||
const unit = req.body.unit;
|
||||
const format = req.body.format || 'short';
|
||||
const all = checkTruthy(req.body.all) || false;
|
||||
const follow = checkTruthy(req.body.follow) || false;
|
||||
const count = checkInt(req.body.count, { positive: true }) || undefined;
|
||||
const unit = req.body.unit;
|
||||
const format = req.body.format || 'short';
|
||||
|
||||
const journald = spawnJournalctl({ all, follow, count, unit, format });
|
||||
res.status(200);
|
||||
journald.stdout.pipe(res);
|
||||
res.on('close', () => {
|
||||
journald.kill('SIGKILL');
|
||||
});
|
||||
journald.on('exit', () => {
|
||||
journald.stdout.unpipe();
|
||||
res.end();
|
||||
});
|
||||
} catch (e) {
|
||||
log.error('There was an error reading the journalctl process', e);
|
||||
if (res.headersSent) {
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
status: 'failed',
|
||||
message: messageFromError(e),
|
||||
});
|
||||
}
|
||||
const journald = spawnJournalctl({ all, follow, count, unit, format });
|
||||
res.status(200);
|
||||
journald.stdout.pipe(res);
|
||||
res.on('close', () => {
|
||||
journald.kill('SIGKILL');
|
||||
});
|
||||
journald.on('exit', () => {
|
||||
journald.stdout.unpipe();
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import * as express from 'express';
|
||||
import { Server } from 'http';
|
||||
import * as _ from 'lodash';
|
||||
@ -102,15 +103,11 @@ export class SupervisorAPI {
|
||||
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)) {
|
||||
throw new Error('Unhealthy');
|
||||
}
|
||||
return res.sendStatus(200);
|
||||
} catch (e) {
|
||||
res.sendStatus(500);
|
||||
const healths = await Promise.all(this.healthchecks.map(fn => fn()));
|
||||
if (!_.every(healths)) {
|
||||
throw new Error('Unhealthy');
|
||||
}
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
this.api.get('/ping', (_req, res) => res.send('OK'));
|
||||
@ -127,19 +124,44 @@ export class SupervisorAPI {
|
||||
// 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, res) => {
|
||||
try {
|
||||
const secret = await this.config.newUniqueKey();
|
||||
await this.config.set({ apiSecret: secret });
|
||||
res.status(200).send(secret);
|
||||
} catch (e) {
|
||||
res.status(503).send(e != null ? e.message : e || 'Unknown error');
|
||||
}
|
||||
const secret = await this.config.newUniqueKey();
|
||||
await this.config.set({ apiSecret: secret });
|
||||
res.status(200).send(secret);
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
log.error(`Error on ${req.method} ${req.path}: `, err);
|
||||
res.status(503).send({
|
||||
status: 'failed',
|
||||
message: messageFromError(err),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async listen(
|
||||
|
Loading…
Reference in New Issue
Block a user