Skip to content

Commit

Permalink
Restructure application setup to allow mocking without magic
Browse files Browse the repository at this point in the history
  • Loading branch information
J12934 committed Oct 9, 2024
1 parent e599943 commit b69ec59
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 594 deletions.
4 changes: 2 additions & 2 deletions juice-balancer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"lint": "eslint src/**",
"test": "NODE_ENV=test jest"
"test": "NODE_ENV=test NODE_OPTIONS='--experimental-vm-modules' jest"
},
"keywords": [],
"author": "iteratec GmbH and multi-juicer contributors",
Expand Down Expand Up @@ -46,4 +46,4 @@
"ui/.*"
]
}
}
}
155 changes: 75 additions & 80 deletions juice-balancer/src/admin/admin.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,92 @@
import { Router } from 'express';

const router = Router();

import {
getJuiceShopInstances,
deletePodForTeam,
deleteDeploymentForTeam,
deleteServiceForTeam,
} from '../kubernetes.js';
import { get } from '../config.js';
import { logger } from '../logger.js';

/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param {import("express").NextFunction} next
*/
function ensureAdminLogin(req, res, next) {
logger.debug('Running admin check');
if (req.teamname === `t-${get('admin.username')}`) {
logger.debug('Admin check succeeded');
return next();
export function createAdminRouteHandler({ kubernetesApi }) {
const router = Router();

/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param {import("express").NextFunction} next
*/
function ensureAdminLogin(req, res, next) {
logger.debug('Running admin check');
if (req.teamname === `t-${get('admin.username')}`) {
logger.debug('Admin check succeeded');
return next();
}
return res.status(401).send();
}
return res.status(401).send();
}

/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function listInstances(req, res) {
logger.debug('Running list all');
const {
body: { items: instances },
} = await getJuiceShopInstances();
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function listInstances(req, res) {
logger.debug('Running list all');
const {
body: { items: instances },
} = await kubernetesApi.getJuiceShopInstances();

return res.json({
instances: instances.map((instance) => {
const team = instance.metadata.labels.team;
return {
team,
name: instance.metadata.name,
ready: instance.status.availableReplicas === 1,
createdAt: instance.metadata.creationTimestamp.getTime(),
lastConnect: parseInt(
instance.metadata.annotations['multi-juicer.owasp-juice.shop/lastRequest'],
10
),
};
}),
});
}
return res.json({
instances: instances.map((instance) => {
const team = instance.metadata.labels.team;
return {
team,
name: instance.metadata.name,
ready: instance.status.availableReplicas === 1,
createdAt: instance.metadata.creationTimestamp.getTime(),
lastConnect: parseInt(
instance.metadata.annotations['multi-juicer.owasp-juice.shop/lastRequest'],
10
),
};
}),
});
}

/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function restartInstance(req, res) {
try {
const teamname = req.params.team;
logger.info(`Restarting deployment for team: '${teamname}'`);
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function restartInstance(req, res) {
try {
const teamname = req.params.team;
logger.info(`Restarting deployment for team: '${teamname}'`);

await deletePodForTeam(teamname);
await kubernetesApi.deletePodForTeam(teamname);

res.send();
} catch (error) {
logger.error(error);
res.status(500).send();
res.send();
} catch (error) {
logger.error(error);
res.status(500).send();
}
}
}

/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function deleteInstance(req, res) {
try {
const teamname = req.params.team;
logger.info(`Deleting deployment for team: '${teamname}'`);
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
*/
async function deleteInstance(req, res) {
try {
const teamname = req.params.team;
logger.info(`Deleting deployment for team: '${teamname}'`);

await deleteDeploymentForTeam(teamname);
await deleteServiceForTeam(teamname);
await kubernetesApi.deleteDeploymentForTeam(teamname);
await kubernetesApi.deleteServiceForTeam(teamname);

res.send();
} catch (error) {
logger.error(error);
res.status(500).send();
res.send();
} catch (error) {
logger.error(error);
res.status(500).send();
}
}
}

router.all('*', ensureAdminLogin);
router.get('/all', listInstances);
router.post('/teams/:team/restart', restartInstance);
router.delete('/teams/:team/delete', deleteInstance);
router.all('*', ensureAdminLogin);
router.get('/all', listInstances);
router.post('/teams/:team/restart', restartInstance);
router.delete('/teams/:team/delete', deleteInstance);

export default router;
return router;
}
145 changes: 73 additions & 72 deletions juice-balancer/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,91 @@ import onFinished from 'on-finished';

import { get } from './config.js';

import teamRoutes from './teams/teams.js';
import adminRoutes from './admin/admin.js';
import proxyRoutes from './proxy/proxy.js';
import scoreBoard from './score-board/score-board.js';
import { createTeamsRouteHandler } from './teams/teams.js';
import { createAdminRouteHandler } from './admin/admin.js';
import { createProxyRouteHandler } from './proxy/proxy.js';
import { createScoreBoardRouteHandler } from './score-board/score-board.js';

const app = express();
export function createApp({ kubernetesApi, proxy }) {
const app = express();

if (get('metrics.enabled')) {
collectDefaultMetrics();
if (get('metrics.enabled')) {
collectDefaultMetrics();

register.setDefaultLabels({ app: 'multijuicer' });
register.setDefaultLabels({ app: 'multijuicer' });

const httpRequestsMetric = new Counter({
name: 'http_requests_count',
help: 'Total HTTP request count grouped by status code.',
labelNames: ['status_code'],
});
const httpRequestsMetric = new Counter({
name: 'http_requests_count',
help: 'Total HTTP request count grouped by status code.',
labelNames: ['status_code'],
});

app.use((req, res, next) => {
onFinished(res, () => {
const statusCode = `${Math.floor(res.statusCode / 100)}XX`;
httpRequestsMetric.labels(statusCode).inc();
app.use((req, res, next) => {
onFinished(res, () => {
const statusCode = `${Math.floor(res.statusCode / 100)}XX`;
httpRequestsMetric.labels(statusCode).inc();
});
next();
});
next();
});

app.get(
'/balancer/metrics',
basicAuth(get('metrics.basicAuth.username'), get('metrics.basicAuth.password')),
async (req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (err) {
console.error('Failed to write metrics', err);
res.status(500).end();
app.get(
'/balancer/metrics',
basicAuth(get('metrics.basicAuth.username'), get('metrics.basicAuth.password')),
async (req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (err) {
console.error('Failed to write metrics', err);
res.status(500).end();
}
}
}
);
}

app.use(cookieParser(get('cookieParser.secret')));
app.use('/balancer', express.json());
app.use((req, res, next) => {
const teamname =
process.env['NODE_ENV'] === 'test'
? req.cookies[get('cookieParser.cookieName')]
: req.signedCookies[get('cookieParser.cookieName')];

req.teamname = teamname;
if (teamname) {
// Omit the initial "t-" part. Example 't-team42' => 'team42'
req.cleanedTeamname = teamname.substring(2);
);
}
next();
});

app.get('/balancer/', (req, res, next) => {
if (req.query['teamname']) {
return next();
}
if (!req.teamname) {
return next();
}
return res.redirect(`/balancer/?msg=logged-in&teamname=${req.cleanedTeamname}`);
});
app.use(cookieParser(get('cookieParser.secret')));
app.use('/balancer', express.json());
app.use((req, res, next) => {
const teamname =
process.env['NODE_ENV'] === 'test'
? req.cookies[get('cookieParser.cookieName')]
: req.signedCookies[get('cookieParser.cookieName')];

req.teamname = teamname;
if (teamname) {
// Omit the initial "t-" part. Example 't-team42' => 'team42'
req.cleanedTeamname = teamname.substring(2);
}
next();
});

app.use('/balancer', express.static(process.env['NODE_ENV'] === 'test' ? 'ui/build/' : 'public'));
app.use(
'/balancer/score-board/',
express.static(process.env['NODE_ENV'] === 'test' ? 'ui/build/' : 'public')
);
app.get('/balancer/', (req, res, next) => {
if (req.query['teamname']) {
return next();
}
if (!req.teamname) {
return next();
}
return res.redirect(`/balancer/?msg=logged-in&teamname=${req.cleanedTeamname}`);
});

app.use('/balancer/teams', teamRoutes);
app.get('/balancer/admin', (req, res) => {
const indexFile = join(
__dirname,
process.env['NODE_ENV'] === 'test' ? '../ui/build/index.html' : '../public/index.html'
app.use('/balancer', express.static(process.env['NODE_ENV'] === 'test' ? 'ui/build/' : 'public'));
app.use(
'/balancer/score-board/',
express.static(process.env['NODE_ENV'] === 'test' ? 'ui/build/' : 'public')
);
res.sendFile(indexFile);
});
app.use('/balancer/admin', adminRoutes);
app.use('/balancer/score-board', scoreBoard);
app.get('/balancer/admin', (req, res) => {
const indexFile = join(
__dirname,
process.env['NODE_ENV'] === 'test' ? '../ui/build/index.html' : '../public/index.html'
);
res.sendFile(indexFile);
});

app.use(proxyRoutes);
app.use('/balancer/teams', createTeamsRouteHandler({ kubernetesApi }));
app.use('/balancer/admin', createAdminRouteHandler({ kubernetesApi }));
app.use('/balancer/score-board', createScoreBoardRouteHandler({ kubernetesApi }));
app.use(createProxyRouteHandler({ kubernetesApi, proxy }));

export default app;
return app;
}
8 changes: 6 additions & 2 deletions juice-balancer/src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { get } from './config.js';
import { logger } from './logger.js';
import app from './app.js';
import { createApp } from './app.js';
import * as kubernetesApi from './kubernetes.js';
import httpProxy from 'http-proxy';

const server = app.listen(get('port'), () =>
const proxy = httpProxy.createProxyServer();

const server = createApp({ kubernetesApi, proxy }).listen(get('port'), () =>
logger.info(`JuiceBalancer listening on port ${get('port')}!`)
);

Expand Down
Loading

0 comments on commit b69ec59

Please sign in to comment.