mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-21 02:01:35 +00:00
Merge pull request #1095 from balena-io/roman/error-handling
Unify API errors processing
This commit is contained in:
commit
b6498fe25a
@ -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…
x
Reference in New Issue
Block a user