Merge pull request #1095 from balena-io/roman/error-handling

Unify API errors processing
This commit is contained in:
Roman Mazur 2019-09-23 19:34:26 +03:00 committed by GitHub
commit b6498fe25a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 275 additions and 357 deletions

View File

@ -935,7 +935,7 @@ export class APIBinder {
router.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); router.use(bodyParser.urlencoded({ limit: '10mb', extended: true }));
router.use(bodyParser.json({ limit: '10mb' })); router.use(bodyParser.json({ limit: '10mb' }));
router.post('/v1/update', (req, res) => { router.post('/v1/update', (req, res, next) => {
apiBinder.eventTracker.track('Update notification'); apiBinder.eventTracker.track('Update notification');
if (apiBinder.readyForUpdates) { if (apiBinder.readyForUpdates) {
this.config this.config
@ -953,15 +953,7 @@ export class APIBinder {
res.sendStatus(202); res.sendStatus(202);
} }
}) })
.catch(err => { .catch(next);
const msg =
err.message != null
? err.message
: err != null
? err
: 'Unknown error';
res.status(503).send(msg);
});
} else { } else {
res.sendStatus(202); res.sendStatus(202);
} }

View File

@ -9,7 +9,7 @@ exports.createV1Api = (router, applications) ->
{ eventTracker } = applications { eventTracker } = applications
router.post '/v1/restart', (req, res) -> router.post '/v1/restart', (req, res, next) ->
appId = checkInt(req.body.appId) appId = checkInt(req.body.appId)
force = checkTruthy(req.body.force) force = checkTruthy(req.body.force)
eventTracker.track('Restart container (v1)', { appId }) eventTracker.track('Restart container (v1)', { appId })
@ -18,10 +18,9 @@ exports.createV1Api = (router, applications) ->
doRestart(applications, appId, force) doRestart(applications, appId, force)
.then -> .then ->
res.status(200).send('OK') res.status(200).send('OK')
.catch (err) -> .catch(next)
res.status(503).send(err?.message or err or 'Unknown error')
v1StopOrStart = (req, res, action) -> v1StopOrStart = (req, res, next, action) ->
appId = checkInt(req.params.appId) appId = checkInt(req.params.appId)
force = checkTruthy(req.body.force) force = checkTruthy(req.body.force)
if !appId? if !appId?
@ -47,16 +46,14 @@ exports.createV1Api = (router, applications) ->
return service return service
.then (service) -> .then (service) ->
res.status(200).json({ containerId: service.containerId }) res.status(200).json({ containerId: service.containerId })
.catch (err) -> .catch(next)
res.status(503).send(err?.message or err or 'Unknown error')
router.post '/v1/apps/:appId/stop', (req, res) -> createV1StopOrStartHandler = (action) -> _.partial(v1StopOrStart, _, _, _, action)
v1StopOrStart(req, res, 'stop')
router.post '/v1/apps/:appId/start', (req, res) -> router.post('/v1/apps/:appId/stop', createV1StopOrStartHandler('stop'))
v1StopOrStart(req, res, 'start') 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) appId = checkInt(req.params.appId)
eventTracker.track('GET app (v1)', { appId }) eventTracker.track('GET app (v1)', { appId })
if !appId? if !appId?
@ -82,10 +79,9 @@ exports.createV1Api = (router, applications) ->
appToSend.commit = status.commit appToSend.commit = status.commit
res.json(appToSend) res.json(appToSend)
) )
.catch (err) -> .catch(next)
res.status(503).send(err?.message or err or 'Unknown error')
router.post '/v1/purge', (req, res) -> router.post '/v1/purge', (req, res, next) ->
appId = checkInt(req.body.appId) appId = checkInt(req.body.appId)
force = checkTruthy(req.body.force) force = checkTruthy(req.body.force)
if !appId? if !appId?
@ -94,6 +90,4 @@ exports.createV1Api = (router, applications) ->
doPurge(applications, appId, force) doPurge(applications, appId, force)
.then -> .then ->
res.status(200).json(Data: 'OK', Error: '') res.status(200).json(Data: 'OK', Error: '')
.catch (err) -> .catch(next)
res.status(503).send(err?.message or err or 'Unknown error')

View File

@ -1,5 +1,5 @@
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { fs } from 'mz'; import { fs } from 'mz';
@ -22,21 +22,10 @@ import { checkInt, checkTruthy } from '../lib/validation';
export function createV2Api(router: Router, applications: ApplicationManager) { export function createV2Api(router: Router, applications: ApplicationManager) {
const { _lockingIfNecessary, deviceState } = applications; 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 = ( const handleServiceAction = (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction,
action: any, action: any,
): Bluebird<void> => { ): Bluebird<void> => {
const { imageId, serviceName, force } = req.body; const { imageId, serviceName, force } = req.body;
@ -85,15 +74,16 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
res.status(200).send('OK'); res.status(200).send('OK');
}); });
}) })
.catch(err => { .catch(next);
res.status(503).send(messageFromError(err));
});
}); });
}; };
const createServiceActionHandler = (action: string) =>
_.partial(handleServiceAction, _, _, _, action);
router.post( router.post(
'/v2/applications/:appId/purge', '/v2/applications/:appId/purge',
(req: Request, res: Response) => { (req: Request, res: Response, next: NextFunction) => {
const { force } = req.body; const { force } = req.body;
const { appId } = req.params; const { appId } = req.params;
@ -101,45 +91,28 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
.then(() => { .then(() => {
res.status(200).send('OK'); res.status(200).send('OK');
}) })
.catch(err => { .catch(next);
let message;
if (err != null) {
message = err.message;
if (message == null) {
message = err;
}
} else {
message = 'Unknown error';
}
res.status(503).send(message);
});
}, },
); );
router.post( router.post(
'/v2/applications/:appId/restart-service', '/v2/applications/:appId/restart-service',
(req: Request, res: Response) => { createServiceActionHandler('restart'),
return handleServiceAction(req, res, 'restart');
},
); );
router.post( router.post(
'/v2/applications/:appId/stop-service', '/v2/applications/:appId/stop-service',
(req: Request, res: Response) => { createServiceActionHandler('stop'),
return handleServiceAction(req, res, 'stop');
},
); );
router.post( router.post(
'/v2/applications/:appId/start-service', '/v2/applications/:appId/start-service',
(req: Request, res: Response) => { createServiceActionHandler('start'),
return handleServiceAction(req, res, 'start');
},
); );
router.post( router.post(
'/v2/applications/:appId/restart', '/v2/applications/:appId/restart',
(req: Request, res: Response) => { (req: Request, res: Response, next: NextFunction) => {
const { force } = req.body; const { force } = req.body;
const { appId } = req.params; const { appId } = req.params;
@ -147,14 +120,14 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
.then(() => { .then(() => {
res.status(200).send('OK'); res.status(200).send('OK');
}) })
.catch(err => { .catch(next);
res.status(503).send(messageFromError(err));
});
}, },
); );
// TODO: Support dependent applications when this feature is complete // TODO: Support dependent applications when this feature is complete
router.get('/v2/applications/state', async (_req: Request, res: Response) => { 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 // It's kinda hacky to access the services and db via the application manager
// maybe refactor this code // maybe refactor this code
Bluebird.join( Bluebird.join(
@ -224,21 +197,24 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
res.status(200).json(response); res.status(200).json(response);
}, },
).catch(next);
},
); );
});
router.get( router.get(
'/v2/applications/:appId/state', '/v2/applications/:appId/state',
(_req: Request, res: Response) => { (_req: Request, res: Response, next: NextFunction) => {
// Get all services and their statuses, and return it // Get all services and their statuses, and return it
applications.getStatus().then(apps => { applications
.getStatus()
.then(apps => {
res.status(200).json(apps); res.status(200).json(apps);
}); })
.catch(next);
}, },
); );
router.get('/v2/local/target-state', async (_req, res) => { router.get('/v2/local/target-state', async (_req, res) => {
try {
const localMode = await deviceState.config.get('localMode'); const localMode = await deviceState.config.get('localMode');
if (!localMode) { if (!localMode) {
return res.status(400).json({ return res.status(400).json({
@ -292,18 +268,11 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
status: 'success', status: 'success',
state: target, state: target,
}); });
} catch (err) {
res.status(503).send({
status: 'failed',
message: messageFromError(err),
});
}
}); });
router.post('/v2/local/target-state', async (req, res) => { router.post('/v2/local/target-state', async (req, res) => {
// let's first ensure that we're in local mode, otherwise // let's first ensure that we're in local mode, otherwise
// this function should not do anything // this function should not do anything
try {
const localMode = await deviceState.config.get('localMode'); const localMode = await deviceState.config.get('localMode');
if (!localMode) { if (!localMode) {
return res.status(400).json({ return res.status(400).json({
@ -328,19 +297,11 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
message: e.message, message: e.message,
}); });
} }
} catch (e) {
const message = 'Could not apply target state: ';
res.status(503).json({
status: 'failed',
message: message + messageFromError(e),
});
}
}); });
router.get('/v2/local/device-info', async (_req, res) => { router.get('/v2/local/device-info', async (_req, res) => {
// Return the device type and slug so that local mode builds can use this to // Return the device type and slug so that local mode builds can use this to
// resolve builds // resolve builds
try {
// FIXME: We should be mounting the following file into the supervisor from the // 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 // start-resin-supervisor script, changed in meta-resin - but until then, hardcode it
const data = await fs.readFile( const data = await fs.readFile(
@ -356,13 +317,6 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
deviceType: deviceInfo.slug, deviceType: deviceInfo.slug,
}, },
}); });
} catch (e) {
const message = 'Could not fetch device information: ';
res.status(503).json({
status: 'failed',
message: message + messageFromError(e),
});
}
}); });
router.get('/v2/local/logs', async (_req, res) => { router.get('/v2/local/logs', async (_req, res) => {
@ -392,15 +346,11 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
router.get('/v2/containerId', async (req, res) => { 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) { if (req.query.serviceName != null || req.query.service != null) {
const serviceName = req.query.serviceName || req.query.service; const serviceName = req.query.serviceName || req.query.service;
const service = _.find( const service = _.find(services, svc => svc.serviceName === serviceName);
services,
svc => svc.serviceName === serviceName,
);
if (service != null) { if (service != null) {
res.status(200).json({ res.status(200).json({
status: 'success', status: 'success',
@ -421,12 +371,6 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
.value(), .value(),
}); });
} }
} catch (e) {
res.status(503).json({
status: 'failed',
message: messageFromError(e),
});
}
}); });
router.get('/v2/state/status', async (_req, res) => { router.get('/v2/state/status', async (_req, res) => {
@ -481,60 +425,36 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
}); });
router.get('/v2/device/name', async (_req, res) => { router.get('/v2/device/name', async (_req, res) => {
try {
const deviceName = await applications.config.get('name'); const deviceName = await applications.config.get('name');
res.json({ res.json({
status: 'success', status: 'success',
deviceName, deviceName,
}); });
} catch (e) {
res.status(503).json({
status: 'failed',
message: messageFromError(e),
});
}
}); });
router.get('/v2/device/tags', async (_req, res) => { router.get('/v2/device/tags', async (_req, res) => {
try {
const tags = await applications.apiBinder.fetchDeviceTags(); const tags = await applications.apiBinder.fetchDeviceTags();
return res.json({ return res.json({
status: 'success', status: 'success',
tags, tags,
}); });
} catch (e) {
res.status(503).json({
status: 'failed',
message: messageFromError(e),
});
}
}); });
router.get('/v2/cleanup-volumes', async (_req, res) => { router.get('/v2/cleanup-volumes', async (_req, res) => {
try {
const targetState = await applications.getTargetApps(); const targetState = await applications.getTargetApps();
const referencedVolumes: string[] = []; const referencedVolumes: string[] = [];
_.each(targetState, app => { _.each(targetState, app => {
_.each(app.volumes, vol => { _.each(app.volumes, vol => {
referencedVolumes.push( referencedVolumes.push(Volume.generateDockerName(vol.appId, vol.name));
Volume.generateDockerName(vol.appId, vol.name),
);
}); });
}); });
await applications.volumes.removeOrphanedVolumes(referencedVolumes); await applications.volumes.removeOrphanedVolumes(referencedVolumes);
res.json({ res.json({
status: 'success', status: 'success',
}); });
} catch (e) {
res.status(503).json({
status: 'failed',
message: messageFromError(e),
});
}
}); });
router.post('/v2/journal-logs', (req, res) => { router.post('/v2/journal-logs', (req, res) => {
try {
const all = checkTruthy(req.body.all) || false; const all = checkTruthy(req.body.all) || false;
const follow = checkTruthy(req.body.follow) || false; const follow = checkTruthy(req.body.follow) || false;
const count = checkInt(req.body.count, { positive: true }) || undefined; const count = checkInt(req.body.count, { positive: true }) || undefined;
@ -551,15 +471,5 @@ export function createV2Api(router: Router, applications: ApplicationManager) {
journald.stdout.unpipe(); journald.stdout.unpipe();
res.end(); 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),
});
}
}); });
} }

View File

@ -1,3 +1,4 @@
import { NextFunction, Request, Response } from 'express';
import * as express from 'express'; import * as express from 'express';
import { Server } from 'http'; import { Server } from 'http';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -102,15 +103,11 @@ export class SupervisorAPI {
this.api.use(expressLogger); this.api.use(expressLogger);
this.api.get('/v1/healthy', async (_req, res) => { this.api.get('/v1/healthy', async (_req, res) => {
try {
const healths = await Promise.all(this.healthchecks.map(fn => fn())); const healths = await Promise.all(this.healthchecks.map(fn => fn()));
if (!_.every(healths)) { if (!_.every(healths)) {
throw new Error('Unhealthy'); throw new Error('Unhealthy');
} }
return res.sendStatus(200); return res.sendStatus(200);
} catch (e) {
res.sendStatus(500);
}
}); });
this.api.get('/ping', (_req, res) => res.send('OK')); 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. // Expires the supervisor's API key and generates a new one.
// It also communicates the new key to the balena API. // It also communicates the new key to the balena API.
this.api.post('/v1/regenerate-api-key', async (_req, res) => { this.api.post('/v1/regenerate-api-key', async (_req, res) => {
try {
const secret = await this.config.newUniqueKey(); const secret = await this.config.newUniqueKey();
await this.config.set({ apiSecret: secret }); await this.config.set({ apiSecret: secret });
res.status(200).send(secret); res.status(200).send(secret);
} catch (e) {
res.status(503).send(e != null ? e.message : e || 'Unknown error');
}
}); });
// And assign all external routers // And assign all external routers
for (const router of this.routers) { for (const router of this.routers) {
this.api.use(router); 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( public async listen(