├── .prettierignore ├── rtran.png ├── .gitignore ├── client ├── assets │ ├── icons │ │ ├── chart.png │ │ ├── node.png │ │ ├── cookies.png │ │ ├── shield.png │ │ └── container.png │ ├── photos │ │ ├── Apps.png │ │ ├── ron.png │ │ ├── Health.png │ │ ├── james.png │ │ ├── mahir.png │ │ ├── pearl.png │ │ ├── Diagram.png │ │ └── aalayah.png │ └── logos │ │ ├── eisodos.png │ │ ├── github.png │ │ └── linkedin.png ├── pages │ ├── DashboardPage │ │ ├── Health │ │ │ ├── health.types.ts │ │ │ ├── components │ │ │ │ ├── PodsMetricsChart.tsx │ │ │ │ ├── CPUBar.tsx │ │ │ │ ├── MemUsageChart.tsx │ │ │ │ ├── CPUUsageChart.tsx │ │ │ │ ├── NetworkReceiveChart.tsx │ │ │ │ └── NetworkTransmitChart.tsx │ │ │ └── Health.tsx │ │ ├── Apps │ │ │ ├── Apps.tsx │ │ │ └── components │ │ │ │ ├── App.tsx │ │ │ │ └── Namespace.tsx │ │ ├── Diagram │ │ │ ├── Legend.tsx │ │ │ └── Diagram.tsx │ │ └── DashboardPage.tsx │ ├── ConnectClusterPage.tsx │ ├── LoginPage.tsx │ ├── RegisterPage.tsx │ └── HomePage.tsx ├── style.css ├── app │ ├── hooks.ts │ └── store.ts ├── index.html ├── index.tsx ├── features │ └── slices │ │ └── errorSlice.ts └── App.tsx ├── .prettierrc ├── jest.config.js ├── server ├── CustomError.ts ├── controllers │ ├── isAuthenticated.ts │ ├── authController.ts │ ├── appsController.ts │ ├── hierarchyController.ts │ └── dashBoardController.ts ├── routes │ ├── apps.ts │ ├── hierarchy.ts │ ├── users.ts │ └── dashboard.ts ├── models │ └── User.ts ├── config │ └── passport.ts ├── service │ └── kubeServ.ts └── server.ts ├── postcss.config.js ├── tsconfig.json ├── .eslintrc.json ├── prometheus.yaml ├── tailwind.config.js ├── webpack.config.ts ├── package.json ├── __tests__ └── routes.test.ts └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.md -------------------------------------------------------------------------------- /rtran.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/rtran.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | dist 5 | .env -------------------------------------------------------------------------------- /client/assets/icons/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/icons/chart.png -------------------------------------------------------------------------------- /client/assets/icons/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/icons/node.png -------------------------------------------------------------------------------- /client/assets/photos/Apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/Apps.png -------------------------------------------------------------------------------- /client/assets/photos/ron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/ron.png -------------------------------------------------------------------------------- /client/assets/icons/cookies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/icons/cookies.png -------------------------------------------------------------------------------- /client/assets/icons/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/icons/shield.png -------------------------------------------------------------------------------- /client/assets/logos/eisodos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/logos/eisodos.png -------------------------------------------------------------------------------- /client/assets/logos/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/logos/github.png -------------------------------------------------------------------------------- /client/assets/photos/Health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/Health.png -------------------------------------------------------------------------------- /client/assets/photos/james.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/james.png -------------------------------------------------------------------------------- /client/assets/photos/mahir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/mahir.png -------------------------------------------------------------------------------- /client/assets/photos/pearl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/pearl.png -------------------------------------------------------------------------------- /client/assets/icons/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/icons/container.png -------------------------------------------------------------------------------- /client/assets/logos/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/logos/linkedin.png -------------------------------------------------------------------------------- /client/assets/photos/Diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/Diagram.png -------------------------------------------------------------------------------- /client/assets/photos/aalayah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/eisodos/HEAD/client/assets/photos/aalayah.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "bracketSameLine": true, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /server/CustomError.ts: -------------------------------------------------------------------------------- 1 | export interface CustomError { 2 | log?: string; 3 | status?: number; 4 | message?: string; 5 | } 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/health.types.ts: -------------------------------------------------------------------------------- 1 | export interface DataPoint { 2 | x: number; 3 | y: number | string; 4 | } 5 | 6 | export interface DataObj { 7 | id: string; 8 | data: DataPoint[]; 9 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); // eslint-disable-line @typescript-eslint/no-var-requires 2 | 3 | module.exports = { 4 | plugins: ['postcss-preset-env', tailwindcss] 5 | }; 6 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | /* import google fonts */ 2 | @import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | -------------------------------------------------------------------------------- /client/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import { RootState, AppDispatch } from './store'; 3 | 4 | // define typed hooks 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /client/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | // import reducers 4 | 5 | export const store = configureStore({ 6 | reducer: {} 7 | }); 8 | 9 | // infer types from the store 10 | export type RootState = ReturnType; 11 | export type AppDispatch = typeof store.dispatch; 12 | -------------------------------------------------------------------------------- /server/controllers/isAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | function isAuthenticated(req: Request, res: Response, next: NextFunction) { 4 | if (req.isAuthenticated()) return next(); 5 | else return res.status(401).json({ message: 'Not authenticated' }); 6 | } 7 | 8 | export default isAuthenticated; 9 | -------------------------------------------------------------------------------- /server/routes/apps.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import isAuthenticated from '../controllers/isAuthenticated'; 3 | import appsController from '../controllers/appsController'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/apps', isAuthenticated, appsController.getPods, (req, res) => { 8 | return res.status(200).json(res.locals.pods); 9 | }); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /server/routes/hierarchy.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import isAuthenticated from '../controllers/isAuthenticated'; 3 | import hierarchyController from '../controllers/hierarchyController'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/tree', isAuthenticated, hierarchyController.showCluster, (req, res) => { 8 | return res.status(200).json(res.locals.cluster); 9 | }); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "jsx": "react", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | //import error? 10 | "allowSyntheticDefaultImports": true, 11 | 12 | // optional config 13 | "sourceMap": true, 14 | "skipLibCheck": true, 15 | "removeComments": true, 16 | "forceConsistentCasingInFileNames": true, 17 | 18 | // needed for typegoose 19 | "experimentalDecorators": true, 20 | "emitDecoratorMetadata": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { store } from './app/store'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import App from './App'; 7 | import './style.css'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container as HTMLElement); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es2021": true 7 | }, 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "sourceType": "module", 11 | "ecmaVersion": "latest", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "plugins": ["@typescript-eslint", "react", "react-hooks", "tailwindcss"], 17 | "extends": [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "plugin:react/recommended", 21 | "plugin:react-hooks/recommended", 22 | "plugin:tailwindcss/recommended", 23 | "prettier" 24 | ], 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Apps/Apps.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import axios from 'axios'; 3 | import Namespace from './components/Namespace'; 4 | 5 | interface AppsData { 6 | [namespace: string]: string[]; 7 | } 8 | 9 | const Apps = () => { 10 | const [data, setData] = useState({}); 11 | 12 | useEffect(() => { 13 | axios.get('/api/cluster/apps').then((res) => { 14 | setData(res.data); 15 | }); 16 | }, []); 17 | 18 | return ( 19 |
20 | {Object.entries(data).map(([namespace, apps]) => ( 21 | 22 | ))} 23 |
24 | ); 25 | }; 26 | 27 | export default Apps; 28 | -------------------------------------------------------------------------------- /client/features/slices/errorSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | 3 | interface ErrorSliceState { 4 | currentUser: null; 5 | errorMessage: string | null | 'p'; 6 | } 7 | 8 | const initialState: ErrorSliceState = { 9 | currentUser: null, 10 | errorMessage: null 11 | }; 12 | 13 | const errorSlice = createSlice({ 14 | name: 'error', 15 | initialState, 16 | reducers: { 17 | setErrorMessage: { 18 | reducer: (state, action: PayloadAction) => { 19 | state.errorMessage = action.payload; 20 | }, 21 | prepare: (errorMessage: string) => { 22 | return { payload: errorMessage }; 23 | } 24 | } 25 | } 26 | }); 27 | 28 | export const { setErrorMessage } = errorSlice.actions; 29 | export default errorSlice.reducer; 30 | -------------------------------------------------------------------------------- /server/routes/users.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import authController from '../controllers/authController'; 3 | import isAuthenticated from '../controllers/isAuthenticated'; 4 | const router = express.Router(); 5 | 6 | // TODO: auth routes have to be protected 7 | 8 | router.post('/register', authController.register, (req, res) => { 9 | res.status(200).send({ message: 'Registration successful.' }); 10 | }); 11 | 12 | router.post('/login', authController.login, (req, res) => { 13 | res.status(200).send({ message: 'Login successful.' }); 14 | }); 15 | 16 | router.post('/logout', authController.logout, (req, res) => { 17 | res.status(200).send({ message: 'Logout successful.' }); 18 | }); 19 | 20 | router.get('/checklogin', isAuthenticated, (req, res) => { 21 | res.status(200).json({ isAuthenticated: true }); 22 | }); 23 | 24 | export default router; 25 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Apps/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | interface AppProps { 5 | name: string; 6 | } 7 | 8 | const App = (props: AppProps) => { 9 | const { name } = props; 10 | return ( 11 |
12 | 13 |
14 | Status 15 | 20 | 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Apps/components/Namespace.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | interface NamespaceProps { 5 | namespace: string; 6 | apps: string[]; 7 | } 8 | 9 | const Namespace = (props: NamespaceProps) => { 10 | const { namespace, apps } = props; 11 | 12 | return ( 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | {/* TODO: use additional data for apps */} 21 | {Object.entries(apps).map(([name, data]) => { 22 | return ; 23 | })} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Namespace; 30 | -------------------------------------------------------------------------------- /server/models/User.ts: -------------------------------------------------------------------------------- 1 | import { prop, getModelForClass, ReturnModelType } from '@typegoose/typegoose'; 2 | 3 | // Below is the typegoose equivalent of: 4 | // const userSchema: Schema = new Schema({ 5 | // username: { type: String, required: true, unique: true }, 6 | // password: { type: String, required: true }, 7 | // cluster: { 8 | // name: { type: String, default: null }, 9 | // }, 10 | // }); 11 | 12 | // TODO: need to figure out what this schema should look like 13 | class User { 14 | @prop({ required: true, unique: true }) 15 | public username!: string; 16 | 17 | @prop({ required: true }) 18 | public password!: string; 19 | 20 | // @prop() 21 | // public cluster?: { 22 | // name?: string; 23 | // }; 24 | } 25 | 26 | const UserModel = getModelForClass(User); 27 | export default UserModel; 28 | export type UserDocument = User & ReturnModelType; 29 | -------------------------------------------------------------------------------- /server/routes/dashboard.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import isAuthenticated from '../controllers/isAuthenticated'; 3 | import dashboardController from '../controllers/dashBoardController'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/metrics', isAuthenticated, dashboardController.getClusterData, (req, res) => { 8 | return res.status(200).json(res.locals.data); 9 | }); 10 | 11 | router.get('/global-metrics', isAuthenticated, dashboardController.getGlobalMetrics, (req, res) => { 12 | return res.status(200).json(res.locals.data); 13 | }); 14 | 15 | router.get( 16 | '/count', 17 | isAuthenticated, 18 | dashboardController.getNumberOf, 19 | (req, res) => { 20 | return res.status(200).json(res.locals.count); 21 | } 22 | ); 23 | 24 | router.get('/global-metrics-percent', isAuthenticated, dashboardController.getGlobalMetricPercentages, (req, res) => { 25 | return res.status(200).json(res.locals.data); 26 | }); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: monitoring.coreos.com/v1 2 | kind: Prometheus 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: prometheus 6 | app.kubernetes.io/instance: k8s 7 | app.kubernetes.io/name: prometheus 8 | app.kubernetes.io/part-of: kube-prometheus 9 | app.kubernetes.io/version: 2.32.1 10 | name: applications 11 | namespace: monitoring 12 | spec: 13 | image: quay.io/prometheus/prometheus:v2.32.1 14 | nodeSelector: 15 | kubernetes.io/os: linux 16 | replicas: 1 17 | resources: 18 | requests: 19 | memory: 400Mi 20 | ruleSelector: {} 21 | securityContext: 22 | fsGroup: 2000 23 | runAsNonRoot: true 24 | runAsUser: 1000 25 | serviceAccountName: prometheus-k8s 26 | #serviceMonitorNamespaceSelector: {} #match all namespaces 27 | serviceMonitorNamespaceSelector: 28 | matchLabels: 29 | kubernetes.io/metadata.name: default 30 | serviceMonitorSelector: {} #match all servicemonitors 31 | version: 2.32.1 32 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | import colors from 'tailwindcss/colors'; 3 | 4 | module.exports = { 5 | content: ['./client/**/*.{html,js,jsx,ts,tsx}'], 6 | theme: { 7 | extend: { 8 | colors: { 9 | black: { 10 | DEFAULT: colors.black, 11 | 1: 'rgb(15,15,20)', 12 | 2: 'rgb(25,25,40)', 13 | 3: 'rgb(40,40,60)' 14 | }, 15 | white: { 16 | DEFAULT: colors.white, 17 | 1: 'rgb(255,255,255)', 18 | 2: 'rgb(215,215,215)', 19 | 3: 'rgb(130,130,130)' 20 | }, 21 | blue: { 22 | DEFAULT: colors.blue, 23 | 1: 'rgb(10, 77, 104)', 24 | 2: 'rgb(0, 43, 91)', 25 | 3: 'rgb(11,173,214)' 26 | }, 27 | highlight: '#1A8BBF', 28 | shadow: '#0D2F4F' 29 | }, 30 | fontFamily: { 31 | inter: ['Inter var', 'sans-serif'], 32 | lato: ['Lato', 'sans-serif'] 33 | }, 34 | boxShadow: { 35 | namespace: '1px 1px 10px #9ab4fb70' 36 | } 37 | } 38 | }, 39 | plugins: [] 40 | }; 41 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | 4 | // import pages 5 | import HomePage from './pages/HomePage'; 6 | import LoginPage from './pages/LoginPage'; 7 | import RegisterPage from './pages/RegisterPage'; 8 | import DashboardPage from './pages/DashboardPage/DashboardPage'; 9 | 10 | // import dashboard components 11 | import Health from './pages/DashboardPage/Health/Health'; 12 | import Diagram from './pages/DashboardPage/Diagram/Diagram'; 13 | import Apps from './pages/DashboardPage/Apps/Apps'; 14 | 15 | // import redux hooks and action creators 16 | import { useAppDispatch, useAppSelector } from './app/hooks'; 17 | 18 | const App = () => { 19 | return ( 20 | <> 21 | 22 | } /> 23 | } /> 24 | } /> 25 | }> 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Diagram/Legend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Legend = () => { 4 | return ( 5 | // Legend container 6 |
7 |
8 | {/*

Legend

*/} 9 |
10 |
11 | 12 | 13 | 14 |
Namespace
15 |
16 |
17 | 18 | 19 | 20 |
Node
21 |
22 |
23 | 24 | 25 | 26 |
Pod
27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Legend; 35 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/PodsMetricsChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | 3 | type MetricProps = { 4 | label: string; 5 | }; 6 | 7 | const PodsMetricsTable = (props: MetricProps) => { 8 | const [metricDivs, setMetricDivs] = useState([]); 9 | 10 | useEffect(() => { 11 | const fetchData = async () => { 12 | try { 13 | const res = await fetch('/api/dashboard/count'); 14 | const data = await res.json(); 15 | 16 | const newMetricDivs: JSX.Element[] = []; 17 | for (const prop in data) { 18 | newMetricDivs.push( 19 |
20 |
{prop}
21 | 22 | {data[prop]} 23 | 24 |
25 | ); 26 | } 27 | setMetricDivs(newMetricDivs); 28 | } catch (error) { 29 | console.log('Error fetching data:', error); 30 | } 31 | }; 32 | fetchData(); 33 | }, []); 34 | 35 | return ( 36 |
37 | {metricDivs} 38 |
39 | ); 40 | }; 41 | 42 | export default PodsMetricsTable; 43 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import HTMLWebpackPlugin from 'html-webpack-plugin'; 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: './client/index.tsx', 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.join(__dirname, '/dist'), 10 | publicPath: '/', 11 | clean: true 12 | }, 13 | // generate source map 14 | devtool: 'source-map', 15 | plugins: [ 16 | // bundle html files 17 | new HTMLWebpackPlugin({ 18 | template: './client/index.html' 19 | }) 20 | ], 21 | devServer: { 22 | // watch for changes to source files 23 | watchFiles: ['client/**/*'], 24 | // proxy for express server 25 | proxy: { 26 | '/api': 'http://localhost:3010' 27 | }, 28 | historyApiFallback: true, 29 | // serves static files 30 | static: { 31 | directory: path.resolve(__dirname, './client/assets'), 32 | publicPath: '/assets' 33 | } 34 | }, 35 | resolve: { 36 | // add ts and tsx as resolvable extensions 37 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 38 | }, 39 | module: { 40 | rules: [ 41 | // babel loaders 42 | { 43 | test: /\.jsx?/, 44 | exclude: /node_modules/, 45 | use: { 46 | loader: 'babel-loader', 47 | options: { 48 | presets: ['@babel/preset-env', '@babel/preset-react'] 49 | } 50 | } 51 | }, 52 | // css and tailwind loaders 53 | { 54 | test: /\.css$/, 55 | use: ['style-loader', 'css-loader', 'postcss-loader'] 56 | }, 57 | // typescript loader 58 | { 59 | test: /\.tsx?$/, 60 | use: 'ts-loader' 61 | } 62 | ] 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /client/pages/ConnectClusterPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ChangeEvent } from 'react'; 2 | 3 | const ConnectClusterPage = () => { 4 | const [cluster, setCluster] = useState(''); 5 | 6 | async function connectCluster(cluster: string) { 7 | try { 8 | const response = await fetch('/api/dashboard/count', { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify({ 14 | apiUrl: cluster, 15 | }), 16 | }); 17 | } catch (error) { 18 | console.error('An error occurred:', error); 19 | } 20 | } 21 | function handleConnectClick() { 22 | connectCluster(cluster); 23 | } 24 | function handleClusterChange(event: ChangeEvent) { 25 | setCluster(event.target.value); 26 | } 27 | return ( 28 |
29 |

Connect to cluster

30 | 36 | 37 |
38 | ); 39 | }; 40 | 41 | // const ConnectClusterPage = () => { 42 | // async function connectCluster(cluster: string) { 43 | // try { 44 | // /* need endpoint for cluster */ 45 | // const response = await fetch('/api/dashboard/count', { 46 | // method: 'POST', 47 | // headers: { 48 | // 'Content-Type': 'Application/JSON', 49 | // }, 50 | // body: JSON.stringify({ 51 | // apiUrl: cluster, 52 | // }), 53 | // }); 54 | // } 55 | // catch (error) { 56 | // console.error('An error occurred:', error); 57 | // } 58 | // } 59 | 60 | // return ( 61 | //
62 | //

Connect to cluster

63 | // 64 | // 65 | //
66 | // ); 67 | // }; 68 | 69 | export default ConnectClusterPage; 70 | 71 | -------------------------------------------------------------------------------- /server/config/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy as LocalStrategy } from 'passport-local'; 3 | import bcrypt from 'bcryptjs'; 4 | import UserModel, { UserDocument } from '../models/User'; 5 | 6 | // Config passport localstrat for username/password 7 | passport.use( 8 | new LocalStrategy(async (username, password, done) => { 9 | try { 10 | // Check if provided username exists in database 11 | const user: UserDocument | null = await UserModel.findOne({ username }); 12 | if (!user) { 13 | return done(null, false, { message: 'Invalid username or password' }); 14 | } 15 | 16 | // Check if provided password matches hashed password 17 | const isMatch: boolean = await bcrypt.compare(password, user.password); 18 | if (!isMatch) { 19 | return done(null, false, { message: 'Invalid username or password' }); 20 | } 21 | 22 | // Auth success, pass user to done callback 23 | return done(null, user); 24 | } catch (err) { 25 | return done(err); 26 | } 27 | }) 28 | ); 29 | 30 | // Serialize the user object to store in the session 31 | // Used to extract a unique identifier from the user object and store it in the session 32 | // This identifier allows Passport.js to recognize the user and retrieve their data when needed 33 | 34 | // TODO: extend Express.User in @types/Express? 35 | interface User { 36 | id?: string; 37 | } 38 | 39 | passport.serializeUser((user: User, done) => { 40 | done(null, user.id); 41 | }); 42 | 43 | // Deserialize the user object from the session 44 | // Needed to fetch the user's information from the database based on the serialized ID stored in the session 45 | // It ensures the user's data is available for auth 46 | passport.deserializeUser(async (id: string, done) => { 47 | try { 48 | const user: UserDocument | null = await UserModel.findById(id); 49 | if (!user) { 50 | return done(null, false); 51 | } 52 | return done(null, user); 53 | } catch (err) { 54 | return done(err); 55 | } 56 | }); 57 | 58 | export default passport; 59 | -------------------------------------------------------------------------------- /server/service/kubeServ.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KubeConfig, 3 | CoreV1Api, 4 | V1PodList, 5 | V1NodeList, 6 | V1NamespaceList, 7 | AppsV1Api, 8 | } from '@kubernetes/client-node'; 9 | 10 | // Connect to the local Kube cluster 11 | const connectToCluster = async (): Promise => { 12 | // Create instance of KubeConfig 13 | const kubeConfig = new KubeConfig(); 14 | 15 | // Load the default cluster configuration 16 | kubeConfig.loadFromDefault(); 17 | 18 | // Create instance of CoreV1Api using config 19 | const k8sApi = kubeConfig.makeApiClient(CoreV1Api); 20 | 21 | // Return CoreV1Api obj to interact with cluster 22 | return k8sApi; 23 | }; 24 | 25 | // List pods in the default namespace 26 | const listPods = async (): Promise => { 27 | // Connect to the local Kube cluster 28 | const k8sApi = await connectToCluster(); 29 | 30 | // Use listNamespacedPod to list pods in the 'default' namespace 31 | const res = await k8sApi.listNamespacedPod('default'); 32 | 33 | // Return list of pods 34 | return res.body; 35 | }; 36 | 37 | // List nodes in the cluster 38 | const listNodes = async (): Promise => { 39 | // Connect to the local Kube cluster 40 | const k8sApi = await connectToCluster(); 41 | 42 | // Use listNode method to list all nodes 43 | const res = await k8sApi.listNode(); 44 | 45 | // Return response containing list of nodes 46 | return res.body; 47 | }; 48 | 49 | const getNamespaces = async (): Promise => { 50 | // Connect to the local Kube cluster 51 | const k8sApi = await connectToCluster(); 52 | 53 | // Use listNamespace method to get all namespaces in the cluster 54 | const res = await k8sApi.listNamespace(); 55 | 56 | // Return list of namespaces 57 | return res.body; 58 | }; 59 | 60 | const getAppsV1ApiClient = (): AppsV1Api => { 61 | const kubeConfig = new KubeConfig(); 62 | kubeConfig.loadFromDefault(); 63 | return kubeConfig.makeApiClient(AppsV1Api); 64 | }; 65 | 66 | export default { 67 | connectToCluster, 68 | listNodes, 69 | listPods, 70 | getNamespaces, 71 | getAppsV1ApiClient, 72 | }; 73 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, Outlet, useLocation } from 'react-router-dom'; 3 | import { Menu } from '@headlessui/react'; 4 | import { ChevronDownIcon } from '@heroicons/react/20/solid'; 5 | 6 | const navigation = [ 7 | { name: 'Health', path: 'health' }, 8 | { name: 'Apps', path: 'apps' }, 9 | { name: 'Diagram', path: 'diagram' } 10 | ]; 11 | 12 | // this is used to highlight the active tab and gray out inactive ones 13 | function isActive(currentPath: string, path: string): string { 14 | if (currentPath === `/dashboard/${path}`) { 15 | return 'text-white-1'; 16 | } else { 17 | return 'text-white-3 hover:text-white'; 18 | } 19 | } 20 | 21 | const DashboardPage = () => { 22 | const location = useLocation(); 23 | 24 | return ( 25 |
26 | {/* sidebar */} 27 | 51 | {/* active tab */} 52 |
53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default DashboardPage; 60 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import mongoose from 'mongoose'; 3 | import cookieParser from 'cookie-parser'; 4 | import session from 'express-session'; 5 | import passport from 'passport'; 6 | import { CustomError } from './CustomError'; 7 | import dotenv from 'dotenv'; 8 | dotenv.config(); 9 | 10 | // Import routers 11 | import usersRouter from './routes/users'; 12 | import dashboardRouter from './routes/dashboard'; 13 | import hierarchyRouter from './routes/hierarchy'; 14 | import appsRouter from './routes/apps'; 15 | 16 | // Assign constants 17 | export const app = express(); 18 | const PORT = 3010; 19 | const mongoURI = process.env.MONGODB_URI; 20 | if (!mongoURI) { 21 | throw new Error('Please set the MONGODB_URI environment variable'); 22 | } 23 | 24 | // Connect to mongo database 25 | mongoose.connect(mongoURI, { dbName: 'test' }); 26 | 27 | // Parse request body 28 | app.use(express.json()); 29 | app.use(express.urlencoded({ extended: true })); 30 | app.use(cookieParser()); 31 | 32 | // Setup session middleware 33 | app.use( 34 | session({ 35 | secret: 'testKey', //TODO: need to add to an env file 36 | resave: false, 37 | saveUninitialized: false 38 | }) 39 | ); 40 | 41 | // Start the passport middleware 42 | require('./config/passport'); // this line should be here 43 | app.use(passport.initialize()); 44 | app.use(passport.session()); 45 | 46 | // Route handlers 47 | app.use('/api/users', usersRouter); 48 | app.use('/api/dashboard', dashboardRouter); 49 | app.use('/api/hierarchy', hierarchyRouter); 50 | app.use('/api/cluster', appsRouter); 51 | 52 | // Unknown route handler 53 | app.use('*', (req, res) => { 54 | return res.status(404).send('404 Not Found'); 55 | }); 56 | 57 | // Global error handler 58 | // TODO: is there a better type to use for Express middleware errors? 59 | app.use((err: Error | CustomError, req: Request, res: Response, next: NextFunction) => { 60 | /* eslint-disable-line */ 61 | const defaultErr = { 62 | log: `Express caught an unknown middleware error: ${err}`, 63 | status: 500, 64 | message: 'Internal Server Error' 65 | }; 66 | 67 | const { log, status, message } = Object.assign({}, defaultErr, err); 68 | 69 | console.log(log); 70 | return res.status(status).send(message); 71 | }); 72 | 73 | // Start server 74 | app.listen(PORT, () => { 75 | console.log(`Server listening on port ${PORT}!`); 76 | }); 77 | -------------------------------------------------------------------------------- /server/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import passport from 'passport'; 3 | import bcrypt from 'bcryptjs'; 4 | 5 | import User, { UserDocument } from '../models/User'; 6 | 7 | const authController = { 8 | register: async (req: Request, res: Response, next: NextFunction) => { 9 | try { 10 | const { username, password } = req.body; 11 | 12 | // Check if username already exists 13 | const existingUser = await User.findOne({ username }); 14 | if (existingUser) { 15 | return res.json({ message: 'Username already exists' }); 16 | } 17 | 18 | // Hash password 19 | const salt = await bcrypt.genSalt(10); 20 | const hashedPassword = await bcrypt.hash(password, salt); 21 | 22 | // Create a new user instance with the hashed password 23 | const newUser = new User({ username, password: hashedPassword }); 24 | 25 | // Save the new user to the database 26 | await newUser.save(); 27 | 28 | return next(); 29 | } catch (error) { 30 | return next({ log: `Error in registration: ${error}` }); 31 | } 32 | }, 33 | 34 | // TODO: use async/await and try/catch here? 35 | login: (req: Request, res: Response, next: NextFunction) => { 36 | passport.authenticate('local', (err: Error, user: UserDocument) => { 37 | // TODO: is Error the correct type to use? 38 | if (err) { 39 | return next({ log: `Error in auth(login): ${err}` }); 40 | } 41 | 42 | // Auth failed, user not found, or password incorrect 43 | if (!user) { 44 | return next({ 45 | log: `Error in auth(userSearch): ${err}`, 46 | status: 400, 47 | message: 'Help' 48 | }); 49 | } 50 | 51 | req.logIn(user, (err) => { 52 | if (err) { 53 | return next({ log: `Error in login: ${err}` }); 54 | } 55 | 56 | // Auth success, user logged in 57 | return next(); 58 | }); 59 | })(req, res, next); 60 | }, 61 | 62 | // This is provided by Passport.js 63 | // Responsible for clearing the user's login session and removing the user's authenticated state 64 | logout: (req: Request, res: Response, next: NextFunction) => { 65 | /* eslint-disable-line */ 66 | req.logout(() => { 67 | res.json({ message: 'logout successful' }); 68 | }); 69 | } 70 | }; 71 | 72 | export default authController; 73 | -------------------------------------------------------------------------------- /server/controllers/appsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as k8s from '@kubernetes/client-node'; 3 | 4 | interface GroupedPods { 5 | [namespace: string]: Namespace; 6 | } 7 | 8 | interface Namespace { 9 | [app: string]: App; 10 | } 11 | 12 | interface App { 13 | status?: string; 14 | } 15 | 16 | const appsController = { 17 | getPods: async (req: Request, res: Response, next: NextFunction): Promise => { 18 | try { 19 | // Initialize Kube config 20 | const kc = new k8s.KubeConfig(); 21 | // Load config from default 22 | kc.loadFromDefault(); 23 | // Create coreV1api instance using the config 24 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 25 | // Retrieve pod list using method 26 | const { body: podList } = await k8sApi.listPodForAllNamespaces(); 27 | // Initialize an empty object to store pod info 28 | const namespaceGroupedPods: GroupedPods = {}; 29 | // Iterate over each pod 30 | for (const pod of podList.items) { 31 | const namespace = pod.metadata?.namespace; 32 | const fullName = pod.metadata?.name; 33 | const status = pod.status?.phase; 34 | 35 | if (!namespace || !fullName || !status) continue; // Skip pods with undefined or unknown namespaces, names 36 | 37 | let baseName = fullName.replace(/-\d.*$/, ''); // String that starts with a hyphen - followed by any digit 38 | 39 | // If baseName is a node exporter pod, strip unique identifier 40 | if (baseName.startsWith('my-monitoring-prometheus-node-exporter')) { 41 | baseName = 'my-monitoring-prometheus-node-exporter'; 42 | } 43 | 44 | if (!namespaceGroupedPods[namespace]) { 45 | namespaceGroupedPods[namespace] = {}; 46 | } 47 | 48 | const appKeys = Object.keys(namespaceGroupedPods[namespace]); 49 | 50 | // Check if the baseName matches the root name of any existing app keys 51 | const matchingKey = appKeys.find((key) => baseName.startsWith(key)); 52 | 53 | if (matchingKey) { 54 | // If a matching key is found, update its status 55 | namespaceGroupedPods[namespace][matchingKey].status = status; 56 | } else { 57 | // If no matching key is found, create a new entry 58 | namespaceGroupedPods[namespace][baseName] = { status }; 59 | } 60 | } 61 | 62 | res.locals.pods = namespaceGroupedPods; 63 | 64 | return next(); 65 | } catch (error) { 66 | return next({ log: `Error in getPods: ${error}` }); 67 | } 68 | } 69 | }; 70 | 71 | export default appsController; 72 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/CPUBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ResponsiveBar } from '@nivo/bar'; 3 | 4 | const CPUResponsiveBar = () => { 5 | return ( 6 |
7 |

CPU Usage %

8 | `${e.id}: ${e.formattedValue} in CPU Usage: ${e.indexValue}`} 53 | theme={{ 54 | axis: { 55 | ticks: { 56 | text: { 57 | fill: '#e5e7eb', 58 | opacity: 0.75 59 | } 60 | }, 61 | legend: { 62 | text: { 63 | fill: '#f3f4f6' 64 | } 65 | } 66 | }, 67 | tooltip: { 68 | container: { 69 | background: '#3b82f6', 70 | opacity: 0.75 71 | }, 72 | basic: { 73 | whiteSpace: 'nowrap', 74 | display: 'flex', 75 | alignItems: 'center' 76 | }, 77 | tableCell: { 78 | fontWeight: 'normal' 79 | }, 80 | tableCellValue: { 81 | fontWeight: 'bold', 82 | color: 'black' 83 | } 84 | } 85 | }} 86 | /> 87 |
88 | ); 89 | }; 90 | 91 | export default CPUResponsiveBar; 92 | -------------------------------------------------------------------------------- /server/controllers/hierarchyController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import * as k8s from '@kubernetes/client-node'; 3 | 4 | interface Node { 5 | name: string; 6 | pods: CustomPod[]; 7 | } 8 | // Had to defined custom interface was getting issues with typescript and what V1pod wanted 9 | interface CustomPod extends k8s.V1Pod { 10 | name: string; 11 | namespace: string; 12 | nodeName: string; 13 | } 14 | 15 | interface Namespace { 16 | name: string; 17 | nodes: Node[]; 18 | } 19 | 20 | interface ClusterHierarchy { 21 | namespaces: Namespace[]; 22 | } 23 | 24 | const hierarchyController = { 25 | showCluster: async (req: Request, res: Response, next: NextFunction): Promise => { 26 | try { 27 | const kc = new k8s.KubeConfig(); 28 | kc.loadFromDefault(); 29 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 30 | // Retrieve the list of nodes, pods, and namespaces from the Kubernetes API 31 | const [nodeList, podList, namespaceList] = await Promise.all([ 32 | k8sApi.listNode(), 33 | k8sApi.listPodForAllNamespaces(), 34 | k8sApi.listNamespace() 35 | ]); 36 | // Create an array to hold the nodes 37 | const nodes: Node[] = nodeList.body.items.map((node) => ({ 38 | name: node.metadata?.name || '', 39 | pods: [] 40 | })); 41 | // Create an array to hold the namespaces 42 | const namespaces: Namespace[] = namespaceList.body.items.map((namespace) => ({ 43 | name: namespace.metadata?.name || '', 44 | nodes: [] 45 | })); 46 | // Create an array to hold the custom pods 47 | const pods: CustomPod[] = podList.body.items.map((pod) => { 48 | const metadata = pod.metadata || {}; 49 | const nodeName = pod.spec?.nodeName || ''; 50 | return { 51 | ...pod, 52 | name: metadata.name || '', 53 | namespace: metadata.namespace || '', 54 | nodeName 55 | }; 56 | }); 57 | 58 | // Assign pods to corresponding nodes 59 | nodes.forEach((node) => { 60 | node.pods = pods.filter((pod) => pod.nodeName === node.name); 61 | }); 62 | 63 | // Assign nodes to corresponding namespaces 64 | namespaces.forEach((namespace) => { 65 | namespace.nodes = nodes.filter((node) => node.pods.some((pod) => pod.namespace === namespace.name)); 66 | }); 67 | 68 | const clusterHierarchy: ClusterHierarchy = { 69 | namespaces 70 | }; 71 | 72 | res.locals.cluster = clusterHierarchy; 73 | return next(); 74 | } catch (error) { 75 | return next({ log: `Error in showCluster: ${error}` }); 76 | } 77 | } 78 | }; 79 | 80 | export default hierarchyController; 81 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/Health.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { DataPoint, DataObj } from './health.types'; 4 | 5 | // import chart components 6 | // TODO: rename these charts 7 | import CPUResponsiveBar from './components/CPUBar'; 8 | import PodsMetricsTable from './components/PodsMetricsChart'; 9 | import CPUUsageChart from './components/CPUUsageChart'; 10 | import MemChart from './components/MemUsageChart'; 11 | import NetworkTransmitChart from './components/NetworkTransmitChart'; 12 | import NetworkReceiveChart from './components/NetworkReceiveChart'; 13 | 14 | // types for fetched data 15 | interface Metrics { 16 | cpu: Metric[]; 17 | memory: Metric[]; 18 | receive: Metric[]; 19 | transmit: Metric[]; 20 | } 21 | 22 | interface Metric { 23 | timestamp: number; 24 | value: string; 25 | } 26 | 27 | const Health = () => { 28 | const [cpuData, setCpuData] = useState([]); 29 | const [memData, setMemData] = useState([]); 30 | const [netTransmitData, setNetTransmitData] = useState([]); 31 | const [netReceiveData, setNetReceiveData] = useState([]); 32 | 33 | useEffect(() => { 34 | getData(); 35 | }, []); 36 | 37 | async function getData(): Promise { 38 | const res = await axios.get('/api/dashboard/global-metrics?time=10m'); 39 | const metrics: Metrics = res.data; 40 | 41 | // format metrics for nivo charts 42 | setCpuData(processChartData(metrics.cpu, 'cpuUsage')); 43 | setMemData(processChartData(metrics.memory, 'memUsage')); 44 | setNetTransmitData(processChartData(metrics.transmit, 'netTransmit')); 45 | setNetReceiveData(processChartData(metrics.transmit, 'netReceive')); 46 | } 47 | 48 | function processChartData(metrics: Metric[], id: string): DataObj[] { 49 | const dataPoints: DataPoint[] = []; 50 | for (let i = 0; i < metrics.length; i++) { 51 | const dataPoint = { 52 | x: metrics[i].timestamp, 53 | y: parseFloat(metrics[i].value) 54 | }; 55 | dataPoints.push(dataPoint); 56 | } 57 | console.log(dataPoints); 58 | return [{ id, data: dataPoints }]; 59 | } 60 | 61 | return ( 62 |
63 |
64 | 65 | 66 |
67 | 68 |
69 |
70 | 71 |
72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Health; 90 | -------------------------------------------------------------------------------- /client/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | 4 | const LoginPage = () => { 5 | const navigate = useNavigate(); 6 | 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | 10 | async function loginUser(event: React.MouseEvent, username: string, password: string) { 11 | event.preventDefault(); 12 | 13 | try { 14 | const response = await fetch('/api/users/login', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'Application/JSON' 18 | }, 19 | body: JSON.stringify({ 20 | username: username, 21 | password: password 22 | }) 23 | }); 24 | 25 | if (response.status === 200) { 26 | // TODO: change this to response.ok? 27 | navigate('/dashboard/health'); // TODO: change this to ':username/dashboard' later 28 | } 29 | } catch (error) { 30 | console.error('An error occurred:', error); 31 | } 32 | } 33 | 34 | return ( 35 |
36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | Log in to your account 44 |
45 | 46 | setUsername(e.target.value)} 50 | className="mb-3 w-full rounded border border-black px-2 py-1 text-black" 51 | /> 52 |
53 |
54 | 55 | setPassword(e.target.value)} 60 | className="mb-4 w-full rounded border border-black px-2 py-1 text-black" 61 | /> 62 |
63 | 69 | 70 | Don't have an account? Sign up here 71 | 72 |
73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | export default LoginPage; 81 | -------------------------------------------------------------------------------- /client/pages/RegisterPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | 4 | const RegisterPage = () => { 5 | const navigate = useNavigate(); 6 | 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | 10 | async function registerUser(event: React.MouseEvent, username: string, password: string) { 11 | event.preventDefault(); 12 | 13 | try { 14 | const response = await fetch('/api/users/register', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | }, 19 | body: JSON.stringify({ 20 | username: username, 21 | password: password 22 | }) 23 | }); 24 | 25 | if (response.ok) { 26 | navigate('/dashboard/health'); // TODO: change this to ':username/:my-cluster' once ConnectClusterPage is done 27 | setUsername(''); 28 | setPassword(''); 29 | } else { 30 | console.log('Sign up request failed:', response); 31 | } 32 | } catch (error) { 33 | console.log('Error occurred while making sign up request:', error); 34 | } 35 | } 36 | 37 | return ( 38 |
39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | Sign up for an account 47 |
48 | 49 | setUsername(e.target.value)} 53 | className="mb-3 w-full rounded border border-black px-2 py-1 text-black" 54 | /> 55 |
56 |
57 | 58 | setPassword(e.target.value)} 63 | className="mb-4 w-full rounded border border-black px-2 py-1 text-black" 64 | /> 65 |
66 | 67 | 73 | 74 | Already have an account? Log in here 75 | 76 |
77 |
78 |
79 |
80 |
81 | ); 82 | }; 83 | 84 | export default RegisterPage; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eisodos", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./client/index.ts", 6 | "scripts": { 7 | "start": "NODE_ENV=development nodemon server/server.ts & NODE_ENV=development webpack-dev-server --open", 8 | "build": "NODE_ENV=production webpack", 9 | "test": "jest" 10 | }, 11 | "author": "James Adler, Pearl Chang, Ron Liu, Mahir Mohtasin, Aalayah Olaes", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@headlessui/react": "^1.7.15", 15 | "@heroicons/react": "^2.0.18", 16 | "@kubernetes/client-node": "^0.18.1", 17 | "@nivo/bar": "^0.83.0", 18 | "@nivo/core": "^0.83.0", 19 | "@nivo/generators": "^0.83.0", 20 | "@nivo/line": "^0.83.0", 21 | "@reduxjs/toolkit": "^1.9.5", 22 | "axios": "^1.4.0", 23 | "bcrypt": "^5.1.0", 24 | "bcryptjs": "^2.4.3", 25 | "cookie-parser": "^1.4.6", 26 | "dotenv": "^16.3.1", 27 | "express": "^4.18.2", 28 | "express-session": "^1.17.3", 29 | "mongodb": "^5.5.0", 30 | "mongoose": "^7.2.2", 31 | "passport": "^0.6.0", 32 | "passport-local": "^1.0.0", 33 | "passport-local-mongoose": "^8.0.0", 34 | "react": "^18.2.0", 35 | "react-d3-tree": "^3.6.1", 36 | "react-dom": "^18.2.0", 37 | "react-force-graph": "^1.43.0", 38 | "react-modal": "^3.16.1", 39 | "react-redux": "^8.0.7", 40 | "react-router-dom": "^6.11.2", 41 | "react-scroll": "^1.8.9", 42 | "react-sizeme": "^3.0.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.21.8", 46 | "@babel/preset-env": "^7.21.5", 47 | "@babel/preset-react": "^7.18.6", 48 | "@typegoose/typegoose": "^11.2.0", 49 | "@types/bcrypt": "^5.0.0", 50 | "@types/bcryptjs": "^2.4.2", 51 | "@types/cookie-parser": "^1.4.3", 52 | "@types/express": "^4.17.17", 53 | "@types/express-session": "^1.17.7", 54 | "@types/jest": "^29.5.2", 55 | "@types/mongoose": "^5.11.97", 56 | "@types/node": "^20.2.5", 57 | "@types/passport": "^1.0.12", 58 | "@types/passport-local": "^1.0.35", 59 | "@types/react": "^18.2.7", 60 | "@types/react-dom": "^18.2.4", 61 | "@types/react-modal": "^3.16.0", 62 | "@types/react-scroll": "^1.8.7", 63 | "@types/supertest": "^2.0.12", 64 | "@typescript-eslint/eslint-plugin": "^5.59.8", 65 | "@typescript-eslint/parser": "^5.59.8", 66 | "babel-loader": "^9.1.2", 67 | "css-loader": "^6.7.3", 68 | "eslint": "^8.41.0", 69 | "eslint-config-prettier": "^8.8.0", 70 | "eslint-plugin-react": "^7.32.2", 71 | "eslint-plugin-react-hooks": "^4.6.0", 72 | "eslint-plugin-tailwindcss": "^3.12.1", 73 | "html-webpack-plugin": "^5.5.1", 74 | "install": "^0.13.0", 75 | "jest": "^29.5.0", 76 | "nodemon": "^2.0.22", 77 | "npm": "^9.6.7", 78 | "postcss": "^8.4.24", 79 | "postcss-loader": "^7.3.3", 80 | "postcss-preset-env": "^8.4.2", 81 | "prettier": "2.8.8", 82 | "prettier-plugin-tailwindcss": "^0.3.0", 83 | "style-loader": "^3.3.2", 84 | "supertest": "^6.3.3", 85 | "tailwindcss": "^3.3.2", 86 | "ts-jest": "^29.1.0", 87 | "ts-loader": "^9.4.3", 88 | "ts-node": "^10.9.1", 89 | "typescript": "^5.1.3", 90 | "webpack": "^5.82.0", 91 | "webpack-cli": "^5.1.1", 92 | "webpack-dev-server": "^4.15.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /__tests__/routes.test.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import request from 'supertest'; 3 | import { app } from '../server/server'; 4 | 5 | jest.mock('../server/controllers/isAuthenticated.ts', () => { 6 | return (req: Request, res: Response, next: NextFunction) => { 7 | return next(); 8 | }; 9 | }); 10 | 11 | jest.mock('../server/controllers/authController.ts', () => { 12 | return { 13 | register: (req: Request, res: Response, next: NextFunction) => { 14 | if (req.body.username === 'existinguser') { 15 | // Simulate registration failure with existing username 16 | res.status(400).json({ message: 'Username already exists' }); 17 | } else { 18 | // Simulate successful registration 19 | res.sendStatus(200); 20 | } 21 | }, 22 | login: (req: Request, res: Response, next: NextFunction) => { 23 | // Simulate login success for a specific username and password 24 | if (req.body.username === 'testuser' && req.body.password === 'correctpassword') { 25 | res.status(200).json({ message: 'Login successful' }); 26 | } else { 27 | // Simulate login failure for incorrect credentials 28 | res.status(400).json({ message: 'Invalid username or password' }); 29 | } 30 | }, 31 | logout: (req: Request, res: Response, next: NextFunction) => { 32 | // Simulate successful logout 33 | res.sendStatus(200); 34 | } 35 | }; 36 | }); 37 | 38 | describe('User authentication routes', () => { 39 | test('should register a new user', async () => { 40 | const response = await request(app).post('/api/users/register'); 41 | expect(response.statusCode).toBe(200); 42 | }); 43 | 44 | test('should fail to register with existing username', async () => { 45 | const response = await request(app) 46 | .post('/api/users/register') 47 | .send({ username: 'existinguser', password: 'testpassword' }); 48 | expect(response.statusCode).toBe(400); 49 | expect(response.body).toEqual({ message: 'Username already exists' }); 50 | }); 51 | 52 | test('should log in a user', async () => { 53 | const response = await request(app) 54 | .post('/api/users/login') 55 | .send({ username: 'testuser', password: 'correctpassword' }); 56 | expect(response.statusCode).toBe(200); 57 | expect(response.body).toEqual({ message: 'Login successful' }); 58 | }); 59 | 60 | test('should fail to log in with incorrect credentials', async () => { 61 | const response = await request(app) 62 | .post('/api/users/login') 63 | .send({ username: 'testuser', password: 'incorrectpassword' }); 64 | expect(response.statusCode).toBe(400); 65 | expect(response.body).toEqual({ message: 'Invalid username or password' }); 66 | }); 67 | 68 | test('should log out a user', async () => { 69 | const response = await request(app).post('/api/users/logout'); 70 | expect(response.statusCode).toBe(200); 71 | }); 72 | }); 73 | 74 | describe('Dashboard routes', () => { 75 | test('responds to /api/dashboard/metrics', async () => { 76 | const response = await request(app).get('/api/dashboard/metrics'); 77 | expect(response.statusCode).toBe(200); 78 | expect(response.body).toBeInstanceOf(Object); 79 | }); 80 | 81 | test('responds to /api/dashboard/count', async () => { 82 | const response = await request(app).get('/api/dashboard/count'); 83 | expect(response.statusCode).toBe(200); 84 | expect(response.body).toBeInstanceOf(Object); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/MemUsageChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { ResponsiveLine } from '@nivo/line'; 3 | import type { DataObj } from '../health.types'; 4 | 5 | interface MemChartProps { 6 | chartData: DataObj[]; // Update the type of chartData according to your data structure 7 | } 8 | 9 | const MemChart = ({ chartData }: MemChartProps) => { 10 | 11 | const formatTime = (timestamp: number) => { 12 | const date = new Date(timestamp * 1000); // Converting to milliseconds 13 | return date.toLocaleTimeString('en-GB'); // You can use toLocaleDateString for dates 14 | }; 15 | 16 | // Preprocessing the data to include formatted timestamps 17 | const formattedData = useMemo(() => { 18 | return chartData.map(series => ({ 19 | ...series, 20 | data: series.data.map(point => ({ 21 | ...point, 22 | x: formatTime(point.x) 23 | })) 24 | })); 25 | }, [chartData]); 26 | 27 | return ( 28 |
29 |

Memory Usage

30 | 108 |
109 | ); 110 | }; 111 | 112 | export default MemChart; 113 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/CPUUsageChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { ResponsiveLine } from '@nivo/line'; 3 | import type { DataObj } from '../health.types'; 4 | 5 | interface CPUUsageChartProps { 6 | chartData: DataObj[]; // Update the type of chartData according to your data structure 7 | } 8 | 9 | const CPUUsageChart = ({ chartData }: CPUUsageChartProps) => { 10 | const formatTime = (timestamp: number) => { 11 | const date = new Date(timestamp * 1000); // Converting to milliseconds 12 | return date.toLocaleTimeString('en-GB'); // You can use toLocaleDateString for dates 13 | }; 14 | 15 | // Preprocessing the data to include formatted timestamps 16 | const formattedData = useMemo(() => { 17 | return chartData.map((series) => ({ 18 | ...series, 19 | data: series.data.map((point) => ({ 20 | ...point, 21 | x: formatTime(point.x) 22 | })) 23 | })); 24 | }, [chartData]); 25 | 26 | return ( 27 |
28 |

CPU Usage

29 | 108 |
109 | ); 110 | }; 111 | 112 | export default CPUUsageChart; 113 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/NetworkReceiveChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { ResponsiveLine } from '@nivo/line'; 3 | import type { DataObj } from '../health.types'; 4 | 5 | interface NetworkReceiveProps { 6 | chartData: DataObj[]; // Update the type of chartData according to your data structure 7 | } 8 | 9 | const NetworkReceiveChart = ({ chartData }: NetworkReceiveProps) => { 10 | 11 | const formatTime = (timestamp: number) => { 12 | const date = new Date(timestamp * 1000); // Converting to milliseconds 13 | return date.toLocaleTimeString('en-GB'); // You can use toLocaleDateString for dates 14 | }; 15 | 16 | // Preprocessing the data to include formatted timestamps 17 | const formattedData = useMemo(() => { 18 | return chartData.map(series => ({ 19 | ...series, 20 | data: series.data.map(point => ({ 21 | ...point, 22 | x: formatTime(point.x) 23 | })) 24 | })); 25 | }, [chartData]); 26 | 27 | return ( 28 |
29 |

Network Received

30 | 108 |
109 | ); 110 | }; 111 | 112 | export default NetworkReceiveChart; 113 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Health/components/NetworkTransmitChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { ResponsiveLine } from '@nivo/line'; 3 | import type { DataObj } from '../health.types'; 4 | 5 | interface NetworkTransmitProps { 6 | chartData: DataObj[]; // Update the type of chartData according to your data structure 7 | } 8 | 9 | const NetworkTransmitChart = ({ chartData }: NetworkTransmitProps) => { 10 | const formatTime = (timestamp: number) => { 11 | const date = new Date(timestamp * 1000); // Converting to milliseconds 12 | return date.toLocaleTimeString('en-GB'); // You can use toLocaleDateString for dates 13 | }; 14 | 15 | // Preprocessing the data to include formatted timestamps 16 | const formattedData = useMemo(() => { 17 | return chartData.map((series) => ({ 18 | ...series, 19 | data: series.data.map((point) => ({ 20 | ...point, 21 | x: formatTime(point.x) 22 | })) 23 | })); 24 | }, [chartData]); 25 | 26 | return ( 27 |
28 |

Network Transmitted

29 | 108 |
109 | ); 110 | }; 111 | 112 | export default NetworkTransmitChart; 113 | -------------------------------------------------------------------------------- /client/pages/DashboardPage/Diagram/Diagram.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import axios from 'axios'; 3 | import { ForceGraph2D } from 'react-force-graph'; 4 | import { SizeMe } from 'react-sizeme'; 5 | import Legend from './Legend'; 6 | 7 | interface Pod { 8 | name: string; 9 | } 10 | 11 | interface Node { 12 | name: string; 13 | pods: Pod[]; 14 | } 15 | 16 | interface Namespace { 17 | name: string; 18 | nodes: Node[]; 19 | } 20 | 21 | interface ClusterHierarchy { 22 | namespaces: Namespace[]; 23 | } 24 | 25 | interface ForceGraphNode { 26 | id: string; 27 | type: string; // differentiate between different node types 28 | x?: number; // x-coordinate 29 | y?: number; // y-coordinate 30 | } 31 | 32 | interface ForceGraphLink { 33 | source: string; 34 | target: string; 35 | } 36 | 37 | const Diagram = () => { 38 | const [clusterData, setClusterData] = useState(null); 39 | const [nodes, setNodes] = useState([]); 40 | const [links, setLinks] = useState([]); 41 | 42 | // Fetch cluster data on component mount 43 | useEffect(() => { 44 | axios 45 | .get('/api/hierarchy/tree') 46 | .then((response) => { 47 | setClusterData(response.data); 48 | }) 49 | .catch((error) => { 50 | console.error('Error fetching cluster data:', error); 51 | }); 52 | }, []); 53 | 54 | // Convert data to format required by graph 55 | const convertData = (data: ClusterHierarchy) => { 56 | // Initialize empty arrays to store nodes, and links 57 | const nodes: ForceGraphNode[] = []; 58 | const links: ForceGraphLink[] = []; 59 | const nodeNames = new Set(); // add a set 60 | 61 | // Loop through the namespaces in data 62 | data.namespaces.forEach((namespace) => { 63 | // Add the namespace as a node 64 | if (!nodeNames.has(namespace.name)) { 65 | // Check if node name already exists 66 | nodes.push({ id: namespace.name, type: 'namespace' }); 67 | nodeNames.add(namespace.name); // Add node name to Set 68 | } 69 | // Loop through the nodes within the namespace 70 | namespace.nodes.forEach((node) => { 71 | // Add node as a node 72 | if (!nodeNames.has(node.name)) { 73 | // Check if node name already exists 74 | nodes.push({ id: node.name, type: 'node' }); 75 | nodeNames.add(node.name); // Add node name to Set 76 | } 77 | // Add a link between the namespace and node 78 | links.push({ source: namespace.name, target: node.name }); 79 | // Loop through the pods within the node 80 | node.pods.forEach((pod) => { 81 | // Add the pod as a node 82 | if (!nodeNames.has(pod.name)) { 83 | // Check if node name already exists 84 | nodes.push({ id: pod.name, type: 'pod' }); 85 | nodeNames.add(pod.name); // Add node name to Set 86 | } 87 | // Add a link between the node and pod 88 | links.push({ source: node.name, target: pod.name }); 89 | }); 90 | }); 91 | }); 92 | // Return the nodes and links 93 | return { nodes, links }; 94 | }; 95 | 96 | // Update nodes and links if clusterData change 97 | useEffect(() => { 98 | if (clusterData) { 99 | const { nodes, links } = convertData(clusterData); 100 | setNodes(nodes); 101 | setLinks(links); 102 | } 103 | }, [clusterData]); 104 | 105 | return ( 106 |
107 | 108 | {/* TODO: make this resize the canvas in real-time */} 109 | 110 | {({ size }) => ( 111 | { 115 | if (typeof node.x === 'number' && typeof node.y === 'number') { 116 | // Make sure node has coords 117 | ctx.beginPath(); // Start path for drawing 118 | if (node.type === 'namespace') { 119 | // If node is namespace 120 | ctx.fillStyle = '#2563eb'; // Set color 121 | ctx.rect(node.x - 10, node.y - 10, 20, 20); // Draw a square 122 | } else if (node.type === 'node') { 123 | // If node is a node 124 | ctx.fillStyle = '#22d3ee'; // Set color 125 | ctx.moveTo(node.x, node.y - 10); // Start triangle path :( 126 | ctx.lineTo(node.x + 10, node.y + 10); // Draw first line of tri 127 | ctx.lineTo(node.x - 10, node.y + 10); // Draw second line of tri 128 | ctx.closePath(); // Draw a triangle 129 | } else { 130 | ctx.fillStyle = '#4ade80'; // If pod set color and draw circle 131 | ctx.arc(node.x, node.y, 10, 0, 2 * Math.PI, false); // Draw a circle 132 | } 133 | ctx.fill(); 134 | } 135 | }} 136 | linkColor={() => '#e1e4e8'} // links color 137 | /> 138 | )} 139 | 140 |
141 | ); 142 | }; 143 | 144 | export default Diagram; 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EISODOS 2 | 3 | 4 | 5 | ### What is Eisodos? 6 | 7 | Eisodos is an open source product that simplifies Kubernetes metric visualization, offering a user-friendly interface. It empowers users to effortlessly monitor and analyze their environments, while providing powerful visualization capabilities for clear insights. 8 | 9 | ## Tech Stacks 10 |
11 | 12 | [![Typescript](https://img.shields.io/badge/Typescript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=black)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) [![React](https://img.shields.io/badge/React-61DAFB?logo=react&logoColor=black)](https://reactjs.org/) [![Redux](https://img.shields.io/badge/Redux-764ABC?logo=redux&logoColor=white)](https://redux.js.org/) [![Redux Toolkit](https://img.shields.io/badge/Redux_Toolkit-7854AA?logo=redux&logoColor=white)](https://redux-toolkit.js.org/) [![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?logo=prometheus&logoColor=white)](https://prometheus.io/) [![Jest](https://img.shields.io/badge/Jest-C21325?logo=jest&logoColor=white)](https://jestjs.io/) [![Git](https://img.shields.io/badge/Git-F05032?logo=git&logoColor=white)](https://git-scm.com/) [![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) [![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white)](https://www.docker.com/) [![Passport.js](https://img.shields.io/badge/Passport.js-34E27A?logo=javascript&logoColor=white)](http://www.passportjs.org/) [![Express.js](https://img.shields.io/badge/Express.js-000000?logo=javascript&logoColor=white)](https://expressjs.com/) [![Kubernetes](https://img.shields.io/badge/Kubernetes-326CE5?logo=kubernetes&logoColor=white)](https://kubernetes.io/) [![Webpack](https://img.shields.io/badge/Webpack-8DD6F9?logo=webpack&logoColor=black)](https://webpack.js.org/) [![Nivo](https://img.shields.io/badge/Nivo-00C4CC?logo=nivo&logoColor=white)](https://nivo.rocks/) [![MongoDB](https://img.shields.io/badge/MongoDB-47A248?logo=mongodb&logoColor=white)](https://www.mongodb.com/) [![HTML](https://img.shields.io/badge/HTML-E34F26?logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/HTML) 13 | 14 | 15 |
16 | 17 | ### Prerequisites: 18 | - Install Docker Desktop on your machine: [https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/) 19 | - Enable Kubernetes in Docker Desktop settings 20 | 21 | Application Instructions: 22 | 23 | This application requires you to run your kubernetes cluster locally. We suggest you use the tool [kind](https://kind.sigs.k8s.io/) to do this. 24 | 25 | 1. Install Kind: 26 | - For Mac: `brew install kind` 27 | 28 | 2. Verify the installation of `kubectl`: 29 | - Run `kubectl --help` to check if you have access to it. 30 | - If not, follow the instructions [here](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/) to install `kubectl`. 31 | 32 | 3. Delete any existing kind cluster: 33 | - List the clusters to delete: `kind get clusters` 34 | - Delete a cluster: `kind delete cluster --name ` 35 | 36 | 4. Ensure Docker is running. 37 | 38 | 5. Create your cluster 39 | - Create a cluster with default settings `kind create cluster` 40 | - if you wish to create a custom cluster please visist the quick start kind [guide](https://kind.sigs.k8s.io/docs/user/ quick-start/#creating-a-cluster). This guide will also serve to help you with configuring your cluster in Kind. 41 | 42 | 43 | Now that we have our default cluster running you can set up prometheus. This guide will set up the version of prometheus that it works best with but you are free to try others as well. If you follow our guide note that you are working with [release .10](https://github.com/prometheus-operator/kube-prometheus/tree/release-0.10) of the kube prometheus repository for version 1.23 of kubernetes. 44 | 45 | 46 | 6. Run your docker container 47 | 48 | `docker run -it -v ${PWD}:/work -w /work alpine sh` 49 | 50 | 7. Adding git to opened container 51 | 52 | `apk add git` 53 | 54 | 8. Shallow cloning file into container 55 | 56 | ``` 57 | # clone 58 | git clone --depth 1 https://github.com/prometheus-operator/kube-prometheus.git -b release-0.10 /tmp/ 59 | 60 | # view the files 61 | ls /tmp/ -l 62 | 63 | # we are interested in the "manifests" folder 64 | ls /tmp/manifests -l 65 | 66 | # let's grab it by copying it out the container 67 | cp -R /tmp/manifests . 68 | ``` 69 | 70 | 9. Exiting 71 | 72 | `exit` 73 | 74 | 10. Apply Manifests 75 | - `kubectl create -f ./manifests/setup/` 76 | - `kubectl create -f ./manifests` 77 | 78 | Please wait for all pods to be in a ready state before continuing (to check this run `kubectl -n monitoring get pods`) 79 | 80 | 11. With your CRDs created from the manifests you gathered you can configure Prometheus with a yaml file. We have provided a template for doing so with the correct versions but you can make changes if you would like. 81 | - if using our YAML file run `kubectl apply -n monitoring -f prometheus.yaml` 82 | 83 | 12. Now that prometheus is set up, you can deploy your application to this cluster using kubectl to do so. Don't forget to add service monitors so that prometheus can scrape metrics for you! 84 | 85 | 13. One last thing! Once your app is running in the cluster and configured to your liking you will need to expose port 9090 to enable metric scraping. 86 | - `kubectl -n monitoring port-forward svc/prometheus-operated 9090` 87 | 88 | 89 | 90 | ### Eisodos Setup Guide 91 | - clone this repository to your machine 92 | - `npm install` to install dependencies 93 | - Set project name, description, and authors in `package.json` 94 | - Add your MongoDB connection string to `mongoURI` in `server.js` 95 | 96 | 97 | 98 | ## The Eisodos Team 99 | 100 | - Aalayah Olaes: [Github](https://github.com/AalayahOlaes) | [LinkedIn](https://www.linkedin.com/in/aalayaholaes/) 101 | - James Adler: [Github](https://github.com/jadler999) | [LinkedIn](https://www.linkedin.com/in/james-adler-/) 102 | - Mahir Mohtasin: [Github](https://github.com/viiewss) | [LinkedIn](https://www.linkedin.com/in/mmohtasin/) 103 | - Ron Liu: [Github](https://github.com/ronliu) | [LinkedIn](https://www.linkedin.com/in/ron-liu/) 104 | - Pearl Chang: [Github](https://github.com/pearlhchang) | [LinkedIn](https://www.linkedin.com/in/pearlhchang/) 105 | -------------------------------------------------------------------------------- /client/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Link as NavLink } from 'react-scroll'; 4 | 5 | const HomePage = () => { 6 | return ( 7 | <> 8 | {/* navbar */} 9 | 46 | 47 | {/* overview */} 48 |
49 |
50 | 51 |
52 |

Eisodos

53 |

Monitor and visualize your key Kubernetes health metrics and cluster data

54 |
55 |
56 |
57 | 58 | {/* features */} 59 |
60 |

Features

61 |
62 | {/* cluster metrics */} 63 |
64 |
65 |

Cluster Metrics

66 | 67 |

68 | Monitor cluster-wide metrics with user-friendly graphs providing clear insights into overall cluster 69 | health. 70 |

71 |
72 |
73 | {/* pod metrics */} 74 |
75 |
76 |

Pod Metrics

77 | 78 |

79 | Keep updated on each pod in your cluster with easily-accessible status and metrics at the pod level. 80 |

81 |
82 |
83 | {/* node hierarchy */} 84 |
85 |
86 |

Node Hierarchy

87 | 88 |

89 | Understand the overall structure of your cluster with a visual diagram of your namespaces, nodes, and 90 | pods. 91 |

92 |
93 |
94 | {/* secure authentication */} 95 |
96 |
97 |

Secure Authentication

98 | 99 |

100 | Robust encryption and hashing techniques to ensure secure authentication. 101 |

102 |
103 |
104 | {/* sessions */} 105 |
106 |
107 |

Sessions

108 | 109 |

110 | Enjoy uninterrupted access to your dashboard with an efficient session management system. 111 |

112 |
113 |
114 |
115 |
116 | 117 | {/* demo */} 118 |
119 |

Demo

120 | {/* dashboard */} 121 |
122 | 123 |
124 |

Dashboard

125 |

126 | Once logged in, you'll see the dashboard. This is where live metrics from your Kubernetes cluster are 127 | shown in a clear and easy-to-understand format. 128 |

129 |
130 |
131 | {/* Applications and Pods */} 132 |
133 | 134 |
135 |

Applications and Pods

136 |

137 | Check out which applications and pods are running. You can see if everything is working fine or if there 138 | are any issues that need your attention. 139 |

140 |
141 |
142 | {/* Hierarchy Graph */} 143 |
144 | 145 |
146 |

Hierarchy Graph

147 |

148 | Explore the hierarchy graph to understand the relationships within your cluster, including how namespaces, 149 | nodes, and pods connect with each other. 150 |

151 |
152 |
153 |
154 | 155 | {/* get started */} 156 |
157 |

Get Started

158 |

Eisodos is easy to use!

159 |

160 | Follow the instructions listed under "Installation" on our{' '} 161 | 162 | GitHub 163 | {' '} 164 | page to get started quickly! 165 |

166 |
167 | 168 | {/* team */} 169 |
170 |

Meet the Team

171 |
172 |
173 | 174 |

Aalayah Olaes

175 | 183 |
184 | 185 |
186 | 187 |

James Adler

188 | 196 |
197 | 198 |
199 | 200 |

Mahir Mohtasin

201 | 209 |
210 | 211 |
212 | 213 |

Pearl Chang

214 | 222 |
223 | 224 |
225 | 226 |

Ron Liu

227 | 235 |
236 |
237 |
238 | 239 | {/* footer */} 240 |
© Eisodos 2023 | MIT License
241 | 242 | ); 243 | }; 244 | 245 | export default HomePage; 246 | -------------------------------------------------------------------------------- /server/controllers/dashBoardController.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import * as k8s from '@kubernetes/client-node'; 4 | import { current } from '@reduxjs/toolkit'; 5 | 6 | // Interfaces define the structure and types of the data received from the API response 7 | interface PromResult { 8 | metric: Record; 9 | 10 | //TODO: MAKE SURE THIS WORKS FOR BOTH TYPES OF RESPONSES 11 | value: [number, string]; 12 | values: [number, string][]; 13 | 14 | } 15 | 16 | interface QueryResult { 17 | resultType: string; 18 | result: PromResult[]; 19 | } 20 | 21 | interface QueryResponse { 22 | status: string; 23 | data: QueryResult; 24 | } 25 | interface DashboardData { 26 | nodes: number; 27 | pods: number; 28 | namespaces: number; 29 | deployments: number; 30 | services: number; 31 | } 32 | 33 | interface GlobalMetrics { 34 | cpu: MetricData[]; 35 | memory: MetricData[]; 36 | transmit: MetricData[]; 37 | receive: MetricData[]; 38 | } 39 | interface MetricData { 40 | timestamp: number; 41 | value: string; 42 | } 43 | 44 | interface PodMetrics { 45 | cpu?: [number, string][]; 46 | memory?: [number, string][]; 47 | } 48 | 49 | interface Pod { 50 | name: string; 51 | podName: string; 52 | namespace: string; 53 | metrics: PodMetrics; 54 | } 55 | 56 | const dashboardController = { 57 | getClusterData: async (req: Request, res: Response, next: NextFunction): Promise => { 58 | // example request: http://localhost:8080/api/dashboard/metrics?time=10m&namespace=monitoring 59 | 60 | let { time, namespace }: { time?: string | undefined; namespace?: string | undefined } = req.query; 61 | if (!time) time = ''; 62 | else time = `[${time}]`; 63 | 64 | if (!namespace) namespace = ''; 65 | else namespace = `,namespace ="${namespace}"`; 66 | 67 | try { 68 | // Retrieve CPU usage data 69 | const responseCpuUsage = await axios.get( 70 | `http://localhost:9090/api/v1/query?query=container_cpu_usage_seconds_total{container!=""${namespace}}${time}` 71 | ); 72 | // Retrieve Mem usage 73 | const responseMemUsage = await axios.get( 74 | `http://localhost:9090/api/v1/query?query=container_memory_usage_bytes{container!=""${namespace}}${time}` 75 | ); 76 | 77 | // Retrieve network transmit 78 | const responseTransmit = await axios.get( 79 | `http://localhost:9090/api/v1/query?query=node_network_transmit_bytes_total{container!=""${namespace}}${time}` 80 | ); 81 | 82 | // Retrieve network receive 83 | const responseReceive = await axios.get( 84 | `http://localhost:9090/api/v1/query?query=node_network_receive_bytes_total{container!=""${namespace}}${time}` 85 | ); 86 | 87 | const formattedResponse: Pod[] = []; 88 | for (let i = 0; i < responseCpuUsage.data.data.result.length; i++) { 89 | //intialize data 90 | let cpuData: [number, string][] = []; 91 | let memoryData: [number, string][] = []; 92 | 93 | //dealing with prometheus auto pluralizing of value property 94 | if (responseCpuUsage.data.data.result[i].values) { 95 | cpuData = responseCpuUsage.data.data.result[i].values; 96 | memoryData = responseMemUsage.data.data.result[i].values; 97 | } 98 | if (responseCpuUsage.data.data.result[i].value) { 99 | cpuData.push(responseCpuUsage.data.data.result[i].value); 100 | memoryData.push(responseMemUsage.data.data.result[i].value); 101 | } 102 | 103 | //creating metrics object 104 | const metrics: PodMetrics = { cpu: cpuData, memory: memoryData }; 105 | 106 | //creating pod objects 107 | const pod: Pod = { 108 | name: responseCpuUsage.data.data.result[i].metric.name, 109 | podName: responseCpuUsage.data.data.result[i].metric.pod, 110 | namespace: responseCpuUsage.data.data.result[i].metric.namespace, 111 | metrics: metrics 112 | }; 113 | 114 | //adding pod to an array to be returned to front end 115 | formattedResponse.push(pod); 116 | } 117 | 118 | res.locals.data = formattedResponse; 119 | 120 | return next(); 121 | } catch (err) { 122 | return next({ log: `Error in dash ${err}` }); 123 | } 124 | }, 125 | 126 | getGlobalMetrics: async (req: Request, res: Response, next: NextFunction): Promise => { 127 | // example request: http://localhost:8080/api/dashboard/global-metrics?time=10m 128 | //destructuring query parameters (no paramaters will get instant metrics) 129 | let { time, namespace }: { time?: string | undefined; namespace?: string | undefined } = req.query; 130 | if (!time) time = ''; 131 | else time = `[${time}]`; 132 | 133 | if (!namespace) namespace = ''; 134 | else namespace = `,namespace ="${namespace}"`; 135 | 136 | //querying for data 137 | try { 138 | //Retrieve CPU usage 139 | 140 | //TODO REMOVE CONTAINER != "" FOR GLOBAL METRICS 141 | const responseCpuUsage = await axios.get( 142 | `http://localhost:9090/api/v1/query?query=container_cpu_usage_seconds_total{container!=""${namespace}}${time}` 143 | ); 144 | // Retrieve Mem usage 145 | const responseMemUsage = await axios.get( 146 | `http://localhost:9090/api/v1/query?query=container_memory_usage_bytes{container!=""${namespace}}${time}` 147 | ); 148 | 149 | // Retrieve network transmit 150 | const responseTransmit = await axios.get( 151 | `http://localhost:9090/api/v1/query?query=node_network_transmit_bytes_total{container!=""${namespace}}${time}` 152 | ); 153 | 154 | // Retrieve network receive 155 | const responseReceive = await axios.get( 156 | `http://localhost:9090/api/v1/query?query=node_network_receive_bytes_total{container!=""${namespace}}${time}` 157 | ); 158 | 159 | //Formatting Response from CPU and Mem response 160 | const formattedResponse: Pod[] = []; 161 | for (let i = 0; i < responseCpuUsage.data.data.result.length; i++) { 162 | //intialize data 163 | let cpuData: [number, string][] = []; 164 | let memoryData: [number, string][] = []; 165 | 166 | //dealing with prometheus auto pluralizing of value property 167 | if (responseCpuUsage.data.data.result[i].values) { 168 | cpuData = responseCpuUsage.data.data.result[i].values; 169 | memoryData = responseMemUsage.data.data.result[i].values; 170 | } 171 | if (responseCpuUsage.data.data.result[i].value) { 172 | cpuData.push(responseCpuUsage.data.data.result[i].value); 173 | memoryData.push(responseMemUsage.data.data.result[i].value); 174 | } 175 | 176 | //creating metrics object 177 | const metrics: PodMetrics = { cpu: cpuData, memory: memoryData }; 178 | 179 | //creating pod objects 180 | const pod: Pod = { 181 | name: responseCpuUsage.data.data.result[i].metric.name, 182 | podName: responseCpuUsage.data.data.result[i].metric.pod, 183 | namespace: responseCpuUsage.data.data.result[i].metric.namespace, 184 | metrics: metrics 185 | }; 186 | 187 | //adding pod to our array for further processing below 188 | formattedResponse.push(pod); 189 | } 190 | 191 | const cpuResult: MetricData[] = []; 192 | 193 | // looping through formatted responses for CPU data and aggregating data across all pods 194 | for (const pod of formattedResponse) { 195 | if (!pod.metrics.cpu) continue; 196 | 197 | for (let i = 0; i < pod.metrics.cpu.length; i++) { 198 | const [timestamp, metric] = pod.metrics.cpu[i]; 199 | 200 | if (!cpuResult[i]) { 201 | cpuResult[i] = { 202 | timestamp: timestamp, 203 | value: metric 204 | }; 205 | } else { 206 | cpuResult[i].value = (parseFloat(cpuResult[i].value) + parseFloat(metric)).toString().slice(0,8); 207 | 208 | } 209 | } 210 | } 211 | 212 | // looping through formatted responses for memory data and aggregating data across all pods 213 | const memResult: MetricData[] = []; 214 | for (const pod of formattedResponse) { 215 | if (!pod.metrics.memory) continue; 216 | 217 | for (let i = 0; i < pod.metrics.memory.length; i++) { 218 | const [timestamp, metric] = pod.metrics.memory[i]; 219 | 220 | //converting to gigabytes 221 | const gigabytes = (parseFloat(metric) / 1000000000).toString() 222 | 223 | if (!memResult[i]) { 224 | memResult[i] = { 225 | timestamp: timestamp, 226 | value: gigabytes 227 | }; 228 | } else { 229 | memResult[i].value = (parseFloat(memResult[i].value) + parseFloat(gigabytes)).toString().slice(0,6); 230 | // while( memResult[i].value.length < 6){ 231 | // memResult[i].value+='0' 232 | // } 233 | } 234 | } 235 | } 236 | 237 | //getting transmit and receive data 238 | const transmitResult: MetricData[] = []; 239 | const receiveResult: MetricData[] = []; 240 | 241 | let transmitData: [number, string][] = []; 242 | let receiveData: [number, string][] = []; 243 | 244 | //dealing with prometheus auto pluralization 245 | let networkingLength = 0 246 | if (responseTransmit.data.data.result[0].values){ 247 | networkingLength = responseTransmit.data.data.result[0].values.length 248 | } 249 | if (responseTransmit.data.data.result[0].value){ 250 | networkingLength = 1 251 | } 252 | 253 | for (let i = 0; i < networkingLength; i++) { 254 | 255 | for (let j = 0 ; j < responseTransmit.data.data.result.length;j++ ){ 256 | //dealing with prometheus auto pluralization again 257 | if (responseCpuUsage.data.data.result[j].values) { 258 | transmitData = responseTransmit.data.data.result[j].values; 259 | receiveData = responseReceive.data.data.result[j].values; 260 | } 261 | if (responseCpuUsage.data.data.result[j].value) { 262 | transmitData.push(responseTransmit.data.data.result[j].value); 263 | receiveData.push(responseReceive.data.data.result[j].value); 264 | } 265 | 266 | 267 | //converting to gigabytes 268 | const transmitGigabytes = (parseFloat(transmitData[i][1])/1000000000).toString() 269 | 270 | //creating object for transmit data 271 | if (!transmitResult[i]) { 272 | transmitResult[i] = { 273 | timestamp: transmitData[i][0], 274 | value: transmitGigabytes 275 | }; 276 | } else { 277 | transmitResult[i].value = (parseFloat(transmitResult[i].value) + parseFloat(transmitGigabytes)).toString().slice(0,6); 278 | } 279 | 280 | const receiveGigabytes = (parseFloat(receiveData[i][1])/1000000000).toString() 281 | 282 | //creating object for receive data 283 | if (!receiveResult[i]) { 284 | receiveResult[i] = { 285 | timestamp: receiveData[i][0], 286 | value: receiveGigabytes 287 | }; 288 | } else { 289 | receiveResult[i].value = (parseFloat(receiveResult[i].value) + parseFloat(receiveGigabytes)).toString().slice(0,6); 290 | } 291 | } 292 | } 293 | 294 | 295 | 296 | //collecting CPU, Memory, Transmit, and Receive into one object to be returned to the front end 297 | const result: GlobalMetrics = { 298 | cpu: cpuResult, 299 | memory: memResult, 300 | transmit: transmitResult, 301 | receive: receiveResult 302 | }; 303 | 304 | // console.log(result) 305 | 306 | res.locals.data = result; 307 | return next(); 308 | } catch (error) { 309 | return next({ log: `Error in getGlobalMetrics: ${error}` }); 310 | } 311 | }, 312 | 313 | 314 | getNumberOf: async (req: Request, res: Response, next: NextFunction): Promise => { 315 | try { 316 | // Create a new KubeConfig 317 | const kc = new k8s.KubeConfig(); 318 | // Load the config from default location 319 | kc.loadFromDefault(); 320 | // Create instance of Corev1api and AppsV1api using the loaded config 321 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 322 | const k8sApi1 = kc.makeApiClient(k8s.AppsV1Api); 323 | // Get all required info for each resource using Promise all 324 | const [ 325 | { body: nodeList }, 326 | { body: podList }, 327 | { body: namespaceList }, 328 | { body: deploymentList }, 329 | { body: serviceList } 330 | ] = await Promise.all([ 331 | k8sApi.listNode(), 332 | k8sApi.listPodForAllNamespaces(), 333 | k8sApi.listNamespace(), 334 | k8sApi1.listDeploymentForAllNamespaces(), 335 | k8sApi.listServiceForAllNamespaces() 336 | ]); 337 | // Create an obj and fill with the counts 338 | const numOfData: DashboardData = { 339 | nodes: nodeList.items.length, 340 | pods: podList.items.length, 341 | namespaces: namespaceList.items.length, 342 | deployments: deploymentList.items.length, 343 | services: serviceList.items.length 344 | }; 345 | // return the counts object in res.locals 346 | res.locals.count = numOfData; 347 | return next(); 348 | } catch (error) { 349 | return next({ log: `Error in getNumberOf: ${error}` }); 350 | } 351 | }, 352 | getGlobalMetricPercentages: async (req: Request, res: Response, next: NextFunction): Promise => { 353 | try { 354 | 355 | const cpuUtilization = await axios.get( 356 | `http://localhost:9090/api/v1/query?query=1 - sum(avg by (mode) (rate(node_cpu_seconds_total{job="node-exporter", mode=~"idle|iowait|steal"}[10m])))` 357 | ); 358 | const memUtilization = await axios.get( 359 | `http://localhost:9090/api/v1/query?query= 1 - sum(:node_memory_MemAvailable_bytes:sum) / sum(node_memory_MemTotal_bytes{job="node-exporter"})` 360 | ); 361 | 362 | const cpuPercent = cpuUtilization.data.data.result[0].value[1] 363 | const memPercent = memUtilization.data.data.result[0].value[1] 364 | 365 | const result = { 366 | cpuPercent:cpuPercent , 367 | memPercent:memPercent 368 | } 369 | 370 | res.locals.data = result; 371 | return next(); 372 | } catch (error) { 373 | return next({ log: `Error in getGlobalMetricPercentages: ${error}` }); 374 | } 375 | } 376 | }; 377 | 378 | export default dashboardController; 379 | 380 | 381 | --------------------------------------------------------------------------------