├── .gitignore ├── client ├── assets │ ├── kubereadylogo.jpg │ └── kubereadylogo_transparent.jpg ├── styles │ ├── _colorpalette.scss │ ├── _general.scss │ ├── _header.scss │ ├── _logout.scss │ ├── styles.scss │ ├── _homepage.scss │ ├── _login_signup.scss │ └── _oldlogin.scss ├── index.html ├── index.js ├── components │ ├── Header.jsx │ ├── Homepage.jsx │ ├── Login.jsx │ └── SignUp.jsx ├── constants │ └── actionTypes.js ├── App.jsx └── containers │ ├── Dashboard.jsx │ └── LogoutContainer.jsx ├── server ├── grafana │ ├── apitoken.json │ └── panels.json ├── controllers │ ├── cookieController.js │ ├── sessionController.js │ ├── grafanaController.js │ ├── userController.js │ └── installController.js ├── models │ ├── sessionModel.js │ └── userModel.js ├── routes │ └── routes.js └── server.js ├── prometheus-grafana.yaml ├── webpack.config.js ├── __tests__ ├── authentication.test.js ├── server.test.js ├── login.test.js └── signup.test.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | package-lock.json -------------------------------------------------------------------------------- /client/assets/kubereadylogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubeready/HEAD/client/assets/kubereadylogo.jpg -------------------------------------------------------------------------------- /client/assets/kubereadylogo_transparent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubeready/HEAD/client/assets/kubereadylogo_transparent.jpg -------------------------------------------------------------------------------- /server/grafana/apitoken.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3, 3 | "name": "apikeycurl2", 4 | "key": "eyJrIjoiNUJNejdyS0hFWXN1OFk4bEx5SjRKc25rSnJEY0FrUnoiLCJuIjoiYXBpa2V5Y3VybDIiLCJpZCI6MX0=" 5 | } 6 | -------------------------------------------------------------------------------- /client/styles/_colorpalette.scss: -------------------------------------------------------------------------------- 1 | //Color Palette - replace colors. 2 | $lightest: #ffffff; 3 | $lighter: #ccf9f8; 4 | $light: #90f3f2; 5 | $mid: #2debeb; 6 | $dark: #276e6e; 7 | $darker: #0d295c; 8 | $black: #1c37a3; 9 | 10 | // kubereadys color palette 11 | $red: #ff3131; 12 | $green: #9cd67c; 13 | $yellow: #ffde59; 14 | -------------------------------------------------------------------------------- /client/styles/_general.scss: -------------------------------------------------------------------------------- 1 | // //The same on every page. 2 | // body { 3 | // background-color: rgb(255, 255, 255); 4 | // margin: 10%; 5 | // padding: 0; 6 | // height: 100%; 7 | // overflow: hidden; 8 | // // display: flex; // commenting out just for testing 9 | // } 10 | 11 | // NOT CURRENTLY USING THIS RIGHT 12 | -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const CookieController = { 4 | // Set the cookie and send it 5 | setCookie: (req, res, next) => { 6 | res.cookie('token', res.locals.user.id, { httpOnly: true }); 7 | return next(); 8 | }, 9 | }; 10 | 11 | module.exports = CookieController; 12 | -------------------------------------------------------------------------------- /client/styles/_header.scss: -------------------------------------------------------------------------------- 1 | // NOT CURRENTLY USING 2 | 3 | .header { 4 | background-color: #000; 5 | width: 100%; 6 | padding: 0; 7 | margin: 0; 8 | 9 | .content { 10 | margin: 0px auto; 11 | width: 100%; 12 | 13 | h1 { 14 | color: white; 15 | } 16 | p { 17 | color: white; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | kubeready 6 | 7 | 8 | 9 |

10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /client/styles/_logout.scss: -------------------------------------------------------------------------------- 1 | //Logout button 2 | #logout-button { 3 | border: none; 4 | background-color: $light; 5 | border-radius: 20px; 6 | padding-top: 5px; 7 | padding-bottom: 5px; 8 | padding-left: 10px; 9 | padding-right: 10px; 10 | margin-right: 5px; 11 | } 12 | 13 | #logout-button { 14 | background-color: rgb(183, 183, 183); 15 | } 16 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App.jsx'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { BrowserRouter, HashRouter } from 'react-router-dom'; 5 | 6 | //declare root 7 | const root = document.getElementById('app'); 8 | ReactDOM.createRoot(root).render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /server/models/sessionModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | //declare schema 4 | const sessionSchema = new mongoose.Schema({ 5 | cookieId: { type: String, required: true, unique: true }, 6 | createdAt: { type: Date, expires: 30, default: Date.now }, 7 | }); 8 | 9 | const Session = mongoose.model('Session', sessionSchema); 10 | module.exports = Session; 11 | -------------------------------------------------------------------------------- /client/styles/styles.scss: -------------------------------------------------------------------------------- 1 | //Import color palette 2 | @import '_colorpalette'; 3 | 4 | //Import the fonts 5 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 6 | @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap'); 7 | 8 | //Import modularized scss files 9 | @import '_general'; 10 | @import '_homepage'; 11 | @import '_login_signup'; 12 | @import '_logout'; 13 | -------------------------------------------------------------------------------- /client/components/Header.jsx: -------------------------------------------------------------------------------- 1 | // Not being used currently 2 | 3 | import React from 'react'; 4 | import kubereadylogo_transparent from '../assets/kubereadylogo_transparent.jpg'; 5 | 6 | const Header = () => { 7 | return ( 8 |
9 |
10 | Header Image 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | // Require in mongoose 2 | const mongoose = require('mongoose'); 3 | 4 | // Create a schema 5 | const userSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | password: { 11 | type: String, 12 | required: true, 13 | }, 14 | username: { 15 | type: String, 16 | required: true, 17 | unique: true, 18 | }, 19 | email: { 20 | type: String, 21 | required: true, 22 | unique: true, 23 | }, 24 | }); 25 | 26 | // Create model 27 | const User = mongoose.model('User', userSchema); 28 | 29 | module.exports = User; 30 | -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | // Define action types for redux 2 | export const FETCH_CPU_UT = 'FETCH_CPU_UT'; 3 | export const FETCH_CPU_REQ = 'FETCH_CPU_REQ'; 4 | export const FETCH_CPU_LIM = 'FETCH_CPU_LIM'; 5 | export const FETCH_MEM_UT = 'FETCH_MEM_UT'; 6 | export const FETCH_MEM_REQ = 'FETCH_MEM_REQ'; 7 | export const FETCH_MEM_LIM = 'FETCH_MEM_LIM'; 8 | export const FETCH_NOD_NUM = 'FETCH_NOD_NUM'; 9 | export const FETCH_NOD_STAT = 'FETCH_NOD_STAT'; 10 | export const FETCH_NOD_READY = 'FETCH_NOD_READY'; 11 | export const FETCH_NOD_NOT_READY = 'FETCH_NOD_NOT_READY'; 12 | export const FETCH_RUN_POD = 'FETCH_RUN_POD'; 13 | export const FETCH_POD_STAT = 'FETCH_POD_STAT'; 14 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './styles/styles.scss'; 3 | import Login from './components/Login.jsx'; 4 | import SignUp from './components/SignUp.jsx'; 5 | import { Routes, Route, Link } from 'react-router-dom'; 6 | import Homepage from './components/Homepage.jsx'; 7 | 8 | const App = () => { 9 | return ( 10 |
11 | 16 | 17 | } /> 18 | } /> 19 | } /> 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /client/containers/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | // Import dependencies 2 | import React from 'react'; 3 | import { useEffect } from 'react'; 4 | import { useNavigate, useLocation } from 'react-router-dom'; 5 | import LogoutContainer from './LogoutContainer.jsx'; 6 | 7 | const Dashboard = () => { 8 | const location = useLocation(); 9 | const data = location?.state?.data; 10 | 11 | const navigate = useNavigate(); 12 | 13 | if (!data) useEffect(() => navigate('/homepage'), []); 14 | else { 15 | // const [url] = Object.entries(data)[0]; 16 | const url = ''; 17 | return ( 18 |
19 | 24 |
25 | ); 26 | } 27 | 28 | return null; 29 | }; 30 | 31 | export default Dashboard; 32 | -------------------------------------------------------------------------------- /client/components/Homepage.jsx: -------------------------------------------------------------------------------- 1 | // Import dependencies 2 | import React from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const Homepage = () => { 6 | return ( 7 |
8 |
9 |

My Dashboard

10 |
11 | 12 | Logout 13 | 14 |
15 |
16 |
17 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Homepage; 28 | -------------------------------------------------------------------------------- /client/styles/_homepage.scss: -------------------------------------------------------------------------------- 1 | //Entire page 2 | #app { 3 | height: 100%; 4 | } 5 | 6 | .dashboard-container { 7 | margin: 0; 8 | padding: 0; 9 | height: 100%; 10 | } 11 | 12 | .dashboard-iframe { 13 | width: 100%; 14 | height: 1000px; 15 | border: none; 16 | } 17 | 18 | .dashboard-header { 19 | display: flex; 20 | justify-content: space-between; 21 | align-items: center; 22 | padding: 20px; 23 | background-color: #111; 24 | color: white; 25 | } 26 | 27 | .header-buttons { 28 | display: flex; 29 | gap: 15px; 30 | } 31 | 32 | .logout-link, 33 | .signup-link { 34 | text-decoration: none; 35 | font-weight: 600; 36 | padding: 5px 10px; 37 | border-radius: 4px; 38 | cursor: pointer; 39 | } 40 | .logout-link { 41 | background-color: $green; 42 | color: #282828; 43 | box-shadow: 0 0 7px rgba(40, 40, 40, 0.2); 44 | } 45 | 46 | .logout-link:hover { 47 | opacity: 0.8; 48 | } 49 | .logout-link:active { 50 | opacity: 0.6; 51 | } 52 | -------------------------------------------------------------------------------- /prometheus-grafana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | # All configs (except security and auth.anonymous) match the kube-prometheus-stack/charts/grafana/templates/configmap.yaml file 4 | grafana.ini: 5 | "[analytics]\ncheck_for_updates = true\n[grafana_net]\nurl = https://grafana.net\n[log]\nmode 6 | = console\n[paths]\ndata = /var/lib/grafana/\nlogs = /var/log/grafana\nplugins 7 | = /var/lib/grafana/plugins\nprovisioning = /etc/grafana/provisioning\n[server]\ndomain 8 | = ''\n[security] \nallow_embedding = true\n[auth.anonymous] \nenabled = true \norg_role 9 | = Admin\n[users] \ndefault_org_role = Admin" 10 | kind: ConfigMap 11 | metadata: 12 | annotations: 13 | meta.helm.sh/release.name: prometheus 14 | meta.helm.sh/release-namespace: default 15 | creationTimestamp: '2023-08-04T15:02:05Z' 16 | labels: 17 | helm.sh/chart: grafana-6.58.6 18 | app.kubernetes.io/version: '10.0.2' 19 | app.kubernetes.io/name: grafana 20 | app.kubernetes.io/instance: prometheus 21 | app.kubernetes.io/managed-by: Helm 22 | name: prometheus-grafana 23 | namespace: default 24 | resourceVersion: '' 25 | uid: 26 | -------------------------------------------------------------------------------- /client/containers/LogoutContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | 4 | const LogoutContainer = () => { 5 | const [isLoading, setIsLoading] = useState(false); 6 | const navigate = useNavigate(); 7 | 8 | // Deletes cookie & bring user back to login 9 | const logoutClick = (e) => { 10 | e.preventDefault(); 11 | setIsLoading(true); 12 | fetch('api/logout', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | }) 18 | .then((response) => { 19 | if (response.ok) { 20 | setIsLoading(false); 21 | navigate('/'); 22 | } else { 23 | console.log('Logout unsuccessful'); 24 | } 25 | }) 26 | .catch((error) => { 27 | setIsLoading(false); 28 | }) 29 | .finally(() => { 30 | setIsLoading(false); 31 | }); 32 | }; 33 | 34 | return ( 35 |
36 | 39 |
40 | ); 41 | }; 42 | 43 | export default LogoutContainer; 44 | -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const Session = require('../models/sessionModel'); 2 | 3 | const SessionController = { 4 | startSession: async (req, res, next) => { 5 | try { 6 | await Session.findOneAndUpdate( 7 | { cookieId: res.locals.user.id }, 8 | { createdAt: Date.now() }, 9 | { upsert: true, setDefaultsOnInsert: true } 10 | ); 11 | return next(); 12 | } catch { 13 | return next({ 14 | log: 'Error occured in sessionController.startSession', 15 | status: 500, 16 | message: 'An error occured in creating/ finding cookie', 17 | }); 18 | } 19 | }, 20 | 21 | checkCookie: (req, res, next) => { 22 | const { cookieId } = req.cookies; 23 | Session.findOne({ cookieId }) 24 | .then((cookie) => { 25 | if (req.cookies.token === cookie) { 26 | res.locals.hasCookie = true; 27 | } else { 28 | res.locals.hasCookie = false; 29 | } 30 | return next(); 31 | }) 32 | .catch((error) => { 33 | return next({ 34 | log: 'Error occured in sessionController.checkCookie', 35 | status: 500, 36 | message: 'An error occured in finding cookie', 37 | }); 38 | }); 39 | }, 40 | }; 41 | 42 | module.exports = SessionController; 43 | -------------------------------------------------------------------------------- /server/routes/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const UserController = require('../controllers/userController.js'); 4 | const CookieController = require('../controllers/cookieController.js'); 5 | const SessionController = require('../controllers/sessionController.js'); 6 | // const grafanaController = require('../controllers/grafanaController.js'); 7 | // const installController = require('../controllers/installController.js'); 8 | 9 | //route handler for a post request to the /signup endpoint 10 | //CREATING A USER ROUTE HANDLER 11 | router.post( 12 | '/signup', 13 | UserController.createUser, 14 | SessionController.startSession, 15 | CookieController.setCookie, 16 | (req, res) => { 17 | return res.status(201).json(res.locals.user); 18 | } 19 | ); 20 | 21 | //route handler for a post req to the /login endpoint 22 | router.post( 23 | '/login', 24 | UserController.verifyUser, 25 | // installController.installPrometheus, 26 | // installController.recreatePromGraf, 27 | // installController.portForward, 28 | // // grafanaController.getApiToken, 29 | // grafanaController.generateDashboard, 30 | // UserController.addUrls, 31 | SessionController.startSession, 32 | CookieController.setCookie, 33 | (req, res) => { 34 | return res.status(201).json(res.locals.user); 35 | } 36 | ); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './client/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'bundle.js', 9 | }, 10 | mode: process.env.NODE_ENV, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?/, 15 | exclude: /(node_modules)/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/preset-env', '@babel/preset-react'], 20 | }, 21 | }, 22 | }, 23 | { 24 | test: /\.s?css/, 25 | use: ['style-loader', 'css-loader', 'sass-loader'], 26 | }, 27 | { 28 | test: /\.(png|jpe?g|gif|svg)$/i, 29 | use: [ 30 | { 31 | loader: 'file-loader?limit=8192', 32 | }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | template: path.resolve(__dirname, './client/index.html'), 41 | }), 42 | ], 43 | devServer: { 44 | static: { 45 | directory: path.resolve(__dirname, './build'), 46 | }, 47 | historyApiFallback: true, 48 | port: 8080, 49 | open: true, 50 | hot: true, 51 | compress: true, 52 | proxy: { 53 | '/api': 'http://localhost:3001', 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /__tests__/authentication.test.js: -------------------------------------------------------------------------------- 1 | // Import the 'supertest' library for testing HTTP endpoints to run tests 2 | const request = require('supertest'); 3 | // Set server URL to localhost 5050 4 | const server = 'http://localhost:5050'; 5 | 6 | // Authentication tests 7 | describe('authentication', () => { 8 | it('should return a valid authentication token when given correct credentials', async () => { 9 | // create mock valid credentials 10 | const validCredentials = { 11 | username: 'valid_user', 12 | password: 'valid_pass', 13 | }; 14 | 15 | // send a POST request to the login endpoint with the valid credentials 16 | const response = await request(server) 17 | .post('/login') 18 | .send(validCredentials); 19 | 20 | // assertion: expect the response body to have a 'token' property equal to 'mock_token' 21 | expect(response.body.token).toEqual('mock_token'); 22 | }); 23 | 24 | // should throw an error when logging in with incorrect credentials 25 | it('should throw an error when logging in with incorrect credentials', async () => { 26 | // create mock invalid credentials 27 | const invalidCredentials = { 28 | username: 'invalid_user', 29 | password: 'invalid_pass', 30 | }; 31 | 32 | // send a POST request to the login endpoint with the invalid credentials 33 | // and expect the server to respond with a 401 status code 34 | const response = await request(server) 35 | .post('/login') 36 | .send(invalidCredentials); 37 | 38 | // assertion: expect the response body to have an 'error' property containing the error message 39 | expect(response.body.error).toEqual('Invalid username and password'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osp1-41", 3 | "version": "1.0.0", 4 | "description": "This is our README - Serena Noels adding stuff.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_ENV=production node server/server.js", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "concurrently \"nodemon server/server.js\" \"NODE_ENV=development webpack serve --open --hot\"" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.22.9", 16 | "@babel/preset-env": "^7.22.9", 17 | "@babel/preset-react": "^7.22.5", 18 | "@testing-library/jest-dom": "^5.17.0", 19 | "@testing-library/react": "^14.0.0", 20 | "@testing-library/user-event": "^14.4.3", 21 | "babel-loader": "^9.1.3", 22 | "css-loader": "^6.8.1", 23 | "file-loader": "^6.2.0", 24 | "html-webpack-plugin": "^5.5.3", 25 | "jest": "^29.6.2", 26 | "msw": "^1.2.3", 27 | "react-router-dom": "^6.14.2", 28 | "sass": "^1.63.6", 29 | "sass-loader": "^13.3.2", 30 | "style-loader": "^3.3.3", 31 | "supertest": "^6.3.3", 32 | "url-loader": "^4.1.1", 33 | "webpack": "^5.88.2", 34 | "webpack-cli": "^5.1.4", 35 | "webpack-dev-server": "^4.15.1" 36 | }, 37 | "dependencies": { 38 | "@reduxjs/toolkit": "^1.9.5", 39 | "bcrypt": "^5.1.0", 40 | "child_process": "^1.0.2", 41 | "concurrently": "^8.2.0", 42 | "express": "^4.18.2", 43 | "jsonwebtoken": "^9.0.1", 44 | "mongodb": "^5.7.0", 45 | "mongoose": "^7.4.1", 46 | "node": "^20.4.0", 47 | "node-fetch": "^3.3.2", 48 | "nodemon": "^3.0.1", 49 | "prom-client": "^14.2.0", 50 | "react": "^18.2.0", 51 | "react-dom": "^18.2.0", 52 | "react-redux": "^8.1.1", 53 | "react-router": "^6.14.2", 54 | "react-router-dom": "^6.14.2", 55 | "redux": "^4.2.1", 56 | "redux-devtools-extension": "^2.13.9", 57 | "spawn-sync": "^2.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | 5 | const router = require('./routes/routes.js'); 6 | const SessionController = require('./controllers/sessionController.js'); 7 | 8 | const PORT = 3001; 9 | 10 | app.use(express.urlencoded({ extended: true })); 11 | app.use(express.json()); 12 | 13 | const mongoose = require('mongoose'); 14 | 15 | mongoose.connect( 16 | 'mongodb+srv://serenahromano2000:E17s30FqKCRZoW5t@cluster0.krvanjb.mongodb.net/', 17 | { useNewUrlParser: true, useUnifiedTopology: true } 18 | ); 19 | mongoose.connection.once('open', () => { 20 | console.log('Connected to Database'); 21 | }); 22 | 23 | app.use(express.static(path.resolve(__dirname, '../build/'))); 24 | 25 | app.get('/', (req, res) => { 26 | res.sendFile(path.resolve(__dirname, '../client/index.html')); 27 | }); 28 | 29 | app.get('/homepage', SessionController.checkCookie, (req, res) => { 30 | if (res.locals.hasCookie) { 31 | res.status(200).redirect('/homepage'); 32 | } else { 33 | console.log('No cookie found - you should not be accessing /homepage'); 34 | res.status(401).send('No cookie found'); 35 | } 36 | }); 37 | 38 | // Sending /api paths to router 39 | app.use('/api', router); 40 | 41 | // Setting content-type header based on file extension 42 | app.use((req, res, next) => { 43 | // Extracting file extension from request URL using "path.extname()" method 44 | const ext = path.extname(req.url); 45 | // Initializing variable content type to store content type 46 | let contentType = ''; 47 | 48 | // Determining content type based on file extension 49 | switch (ext) { 50 | case '.css': 51 | contentType = 'text/css'; 52 | break; 53 | case '.js': 54 | contentType = 'application/javascript'; 55 | break; 56 | case '.yaml': 57 | contentType = 'text/yaml'; 58 | break; 59 | } 60 | 61 | // Setting content-type header in response 62 | res.setHeader('Content-Type', contentType); 63 | // Move to next middleware in chain 64 | next(); 65 | }); 66 | 67 | // In future updates, logout can be moved to routes.js 68 | router.post('/api/logout', (req, res) => { 69 | return res.status(201); 70 | }); 71 | 72 | // Catch-all error handler 73 | app.use('*', (req, res) => { 74 | // Sends 404 status and display a message 75 | res.status(404).send('Page not found.'); 76 | }); 77 | 78 | // Global error handler 79 | app.use((err, req, res, next) => { 80 | // Defines default error object 81 | const defaultErr = { 82 | log: 'Express error handler caught unknown middleware error', 83 | status: 500, 84 | message: { err: 'An error has occurred' }, 85 | }; 86 | // Merges default error object with error received 87 | const errorObj = Object.assign({}, defaultErr, err); 88 | // Logs error message to console 89 | console.log(errorObj.log); 90 | // Sends the error response with status code and message 91 | return res.status(errorObj.status).json(errorObj.message); 92 | }); 93 | 94 | // Listening on port 95 | app.listen(PORT, () => { 96 | console.log(`Server is running on port: ${PORT}`); 97 | }); 98 | -------------------------------------------------------------------------------- /server/controllers/grafanaController.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require('child_process'); 2 | const apitoken = require('../grafana/apitoken.json'); 3 | const panels = require('../grafana/panels.json'); 4 | 5 | //initialize an empty object that will house dashboard URL 6 | const urlStorage = {}; 7 | 8 | const grafanaController = { 9 | getApiToken: (req, res, next) => { 10 | console.log('entered getApiToken in Grafana Controller'); 11 | 12 | const getToken = spawnSync( 13 | 'curl -s -X POST -H "Content-Type: application/json" -H "Cookie: grafana_session=$session" -d \'{"name":"apikeycurl0", "role": "Admin"}\' http://localhost:3000/api/auth/keys > grafana/apitoken.json', 14 | { stdio: 'inherit', shell: true } 15 | ); 16 | 17 | if (getToken.stderr) { 18 | console.log(`getting grafana API token error: ${getToken.stderr}`); 19 | return next({ 20 | log: 'Error on grafanaController.getApiToken middleware.', 21 | status: 500, 22 | message: { 23 | err: 'An error occurred when trying to get the grafana API token.', 24 | }, 25 | }); 26 | } 27 | 28 | return next(); 29 | }, 30 | //add generateDashboard method that takes in 3 objects: req, res, next 31 | generateDashboard: (req, res, next) => { 32 | console.log('entered generateDashboard in Grafana Controller'); 33 | //tracks the creation of a new dashboard 34 | res.locals.generatedDash = false; 35 | 36 | //if there's already a url, skip 37 | if (res.locals.URL) return next(); 38 | 39 | //Send a POST request with details about the new dashboard. 40 | fetch('http://admin:prom-operator@localhost:3000/api/dashboards/db', { 41 | //add to method param of fetch req: to specify which HTTP method used in request 42 | method: 'POST', 43 | headers: { 44 | Accept: 'application/json', 45 | 'Content-Type': 'application/json', 46 | Authorization: `Bearer ${apitoken.key}`, 47 | }, 48 | body: JSON.stringify({ 49 | dashboard: { 50 | id: null, 51 | uid: null, 52 | title: 'kubeready', 53 | tags: ['templated'], 54 | timezone: 'browser', 55 | schemaVersion: 16, 56 | version: 0, 57 | refresh: '25s', 58 | panels: panels, 59 | }, 60 | folderID: 0, 61 | message: '', 62 | overwrite: false, 63 | }), 64 | }) 65 | .then((data) => data.json()) 66 | .then((data) => { 67 | console.log('retrieved data back from POST request'); 68 | //Create a dashboard URL from the returned data. 69 | const { uid } = data; 70 | urlStorage.dashboard = `http://localhost:3000/d/${uid}/kubereadyDashboard?orgId=1&refresh=5s`; 71 | res.locals.generatedDash = true; 72 | console.log('a dashboard has been created'); 73 | 74 | return next(); 75 | }) 76 | .catch((error) => { 77 | return next({ 78 | log: `could not fetch data or resolve promise to the Grafana API, ${error}`, 79 | status: 500, 80 | message: { 81 | err: error, 82 | }, 83 | }); 84 | }); 85 | }, 86 | }; 87 | 88 | module.exports = grafanaController; 89 | -------------------------------------------------------------------------------- /__tests__/server.test.js: -------------------------------------------------------------------------------- 1 | // Require in dependencies to run tests to run tests 2 | // Import the 'supertest' library for testing HTTP endpoints 3 | const request = require('supertest'); 4 | // set server URL to localhost 5050 5 | const server = 'http://localhost:5050'; 6 | // generate test username with the current timestamp 7 | const testUserName = `test${Date.now()}`; 8 | // define varable to hold auth token for the test user 9 | let authToken; 10 | 11 | // before each test, perform a signup request to create a test user 12 | beforeEach(async () => { 13 | // send a POST request to the signup endpoint with the test username and password and return the result as a promise 14 | await request(server) 15 | .post('/signup') 16 | .send({ username: testUserName, password: '12345' }); 17 | }); 18 | 19 | // after all tests are completed, delete the test user created during signup 20 | afterEach(async () => { 21 | // send a POST request to the delete endpoint with the test username and password and return the result as a promise 22 | await request(server) 23 | .post('/delete') 24 | .send({ username: testUserName, password: '12345' }); 25 | }); 26 | 27 | // backend tests 28 | describe('backend', () => { 29 | // signup route tests 30 | describe('signup route', () => { 31 | it('should return 400 if username already exists', () => { 32 | // send a POST request to the signup endpoint with the same test username and expect the response status code to be 400 and the content-type header to be application/json 33 | return request(server) 34 | .post('/signup') 35 | .send({ username: testUserName, password: '12345' }) 36 | .expect(400) 37 | .expect('Content-Type', /application\/json/); 38 | }); 39 | 40 | it('should return specific error message for invalid signup', () => { 41 | // send a POST request to the signup endpoint with an invalid payload (missing password) 42 | return request(server) 43 | .post('/signup') 44 | .send({ username: 'test' }) 45 | .expect(422) 46 | .expect('Content-Type', /application\/json/) 47 | .expect((res) => { 48 | // expect the response body to contain an 'error' property with a specific error message 49 | expect(res.body.error).toEqual('Password is required.'); 50 | }); 51 | }); 52 | }); 53 | 54 | // login route tests 55 | describe('login route', () => { 56 | it('should successfullyl login to a test account', () => { 57 | // send a POST request to the login endpoint with the test username and password and expect the response status code to be 201 and the content-type header to be application/json 58 | return request(server) 59 | .post('/login') 60 | .send({ username: testUserName, password: '12345' }) 61 | .expect(201) 62 | .expect('Content-Type', /application\/json/) 63 | .then((res) => { 64 | // store auth token for further tests 65 | authToken = res.body.token; 66 | }); 67 | }, 25000); // set a timeout of 25000 ms (25 s) 68 | 69 | // **NOTE**: noticed that the test below is essentially testing the same functionality as a test on the login test file 70 | 71 | // it('should return 401 with invalid user credentials', () => { 72 | // // send a POST request to the login endpoint with the invalid user credentials and expect the response status code to be 401 and the content-type header to be application/json 73 | // return request(server) 74 | // .post('/login') 75 | // .send({ username: 'test' }) 76 | // .expect(401) 77 | // .expect('Content-Type', /application\/json/) 78 | // }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /__tests__/login.test.js: -------------------------------------------------------------------------------- 1 | // import necessary dependencies to run tests 2 | import React from 'react'; 3 | import { render, screen, waitFor } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import { MemoryRouter } from 'react-router'; 6 | 7 | // Import the login function (or whatever it's actually called) 8 | const {} = require(''); 9 | 10 | beforeAll(() => { 11 | // Listen for server 12 | server.listen(); 13 | }); 14 | 15 | afterEach(() => { 16 | server.resetHandlers(); 17 | }); 18 | 19 | afterAll(() => { 20 | server.close(); 21 | }); 22 | 23 | // unit tests for the login function 24 | describe('login page', () => { 25 | describe('rendering', () => { 26 | beforeEach(() => { 27 | render( 28 | 29 | 30 | 31 | ); 32 | }); 33 | test('check if username input is on the login page', () => { 34 | expect(screen.getByPlaceholderText('Username')).toBeInTheDocument(); 35 | }); 36 | 37 | test('check if password input is on the login page', () => { 38 | expect(screen.getByPlaceholderText('Password')).toBeInTheDocument(); 39 | }); 40 | 41 | test('check if login button is on the login page', () => { 42 | // create login button 43 | const loginButton = screen.getbyRole('button', { name: 'Login' }); 44 | expect(loginButton).toBeInTheDocument(); 45 | }); 46 | describe('behavior', () => { 47 | beforeEach(() => { 48 | render( 49 | 50 | 51 | 52 | ); 53 | }); 54 | // **note** - the test below is an integration test, considering that we're testing multiple components 55 | test('user is navigated to dashboard after entering correct login credentials', async () => { 56 | // create a login button, username, and pass 57 | const loginButton = screen.getByRole('button', { name: 'Login' }); 58 | const userInput = screen.getByPlaceholderText('Username'); 59 | const passwordInput = screen.getByPlaceholderText('Password'); 60 | // user types in correct user and pass 61 | await userEvent.type(userInput, 'testuser'); 62 | await userEvent.type(passwordInput, 'testpassword'); 63 | // assert the correct user and pass 64 | expect(userInput.value).toBe('testuser'); 65 | expect(passwordInput.value).toBe('testpassword'); 66 | // user clicks login button 67 | await userEvent.click(loginButton); 68 | // assert that the dashboard is in the document 69 | waitFor(async () => { 70 | // *NOTE* REPLACE IFRAME NAME HERE 71 | await screen.findByTitle('NAME OF IFRAME'); 72 | // assertion: expect the iframe to be in the document 73 | expect(screen.findByTitle('NAME OF THE IFRAME')).toBeInTheDocument(); 74 | }); 75 | }); 76 | 77 | // **IF WE HAVE TIME, FINISH THIS ONE BELOW** 78 | // test ('user sees error message after login in with incorrect credentials', async () => { 79 | // // creat a login button, username, and pass 80 | // const loginButton = screen.getByRole('button', {name: 'Login'}); 81 | // const userInput = screen.getByPlaceholderText('Username') 82 | // const passwordInput = screen.getByPlaceholderText('Password'); 83 | // // user types in incorrect user and pass 84 | // await userEvent.type(userInput, 'incorrectuser'); 85 | // await userEvent.type(passwordInput, 'incorrectpassword'); 86 | // // assert the incorrect user and pass 87 | 88 | // // user clicks login button 89 | // await userEvent.click(loginButton) 90 | // // assert that the error message appears on page 91 | // expect(screen.findByTitle('incorrect username'))) 92 | // // assert that the user is taken back to the login page 93 | 94 | // }) 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const User = require('../models/userModel'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const UserController = { 6 | createUser: async (req, res, next) => { 7 | try { 8 | const { name, password, username, email } = req.body; 9 | 10 | // Check if the username already exists 11 | const existingUser = await User.findOne({ username }); 12 | if (existingUser) { 13 | res.send('This username already exists.'); 14 | return next(); 15 | } 16 | 17 | // Hashes the password before saving it to the database 18 | const hashedPassword = await bcrypt.hash(password, 10); 19 | 20 | const newUser = new User({ 21 | name, 22 | password: hashedPassword, 23 | username, 24 | email, 25 | }); 26 | 27 | // Saves the new user to the database 28 | const savedUser = await newUser.save(); 29 | res.locals.user = savedUser; 30 | 31 | return next(); 32 | } catch (error) { 33 | return next(error); 34 | } 35 | }, 36 | 37 | verifyUser: (req, res, next) => { 38 | const { username, password } = req.body; 39 | if (!username || !password) { 40 | return next( 41 | 'Error - both username and password should be provided for login.' 42 | ); 43 | } 44 | User.findOne({ username }) 45 | .then((user) => { 46 | // If username is not found: 47 | if (!user) { 48 | return next('Username or password is not found.'); 49 | // If the username is found: 50 | } else { 51 | // Uses bcrypt compare to compare passwords 52 | bcrypt.compare(password, user.password).then((result) => { 53 | // If stored passwords do not match, return error 54 | if (!result) { 55 | return next('Username or password is not found.'); 56 | } else { 57 | res.locals.user = user; 58 | if (user.urls) res.locals.URLS = user.urls[0]; 59 | return next(); 60 | } 61 | }); 62 | } 63 | }) 64 | .catch((err) => { 65 | return next({ 66 | log: `userController.verifyUser ERROR: ${err}`, 67 | status: 500, // Internal server error code 68 | message: { 69 | error: 'Error in finding the username or password', 70 | }, 71 | }); 72 | }); 73 | }, 74 | addUrls: (req, res, next) => { 75 | if (res.locals.generatedDash === false) { 76 | return next(); 77 | } 78 | const userID = res.locals.user.id; 79 | User.findOneAndUpdate({ _id: userID }, { $push: { urls: res.locals.URLS } }) 80 | .then((user) => { 81 | return next(); 82 | }) 83 | .catch((err) => { 84 | next({ 85 | log: 'error!', 86 | status: 500, 87 | message: { err: 'An error occured while adding URLs' }, 88 | }); 89 | }); 90 | }, 91 | }; 92 | module.exports = UserController; 93 | 94 | //VERIFY USER B4 COOKIE STUFF 95 | // verifyUser: (req, res, next) => { 96 | // const { username, password } = req.body; 97 | // // console.log(req.body, 'reqbody'); 98 | // // console.log(username, 'username'); 99 | // if (!username || !password) { 100 | // return next( 101 | // 'Error - both username and password should be provided for login.' 102 | // ); 103 | // } 104 | // User.findOne({ username }) 105 | // .then((user) => { 106 | // //if username is not found 107 | // if (!user) { 108 | // return next('Username or password is not found.'); 109 | // //ow, if the username is found 110 | // } else { 111 | // bcrypt.compare(password, user.password).then((result) => { 112 | // //if the stored passwords do not match, return error 113 | // if (!result) { 114 | // return next('Username or password is not found.'); 115 | // } else { 116 | // //if the stored passwords match, save user in res.locals 117 | // res.locals.user = user; 118 | // console.log('it worked'); 119 | // return next(); 120 | // } 121 | // }); 122 | // } 123 | // }) 124 | // .catch((err) => { 125 | // return next({ 126 | // log: `userController.verifyUser ERROR: ${err}`, 127 | // status: 500, // internal server error 128 | // message: { 129 | // error: 'Error in finding the username or password', 130 | // }, 131 | // }); 132 | // }); 133 | // }, 134 | 135 | module.exports = UserController; 136 | -------------------------------------------------------------------------------- /client/styles/_login_signup.scss: -------------------------------------------------------------------------------- 1 | // container that holds the login-input/createAcct button 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | font-family: 'Quicksand', sans-serif; 7 | } 8 | body { 9 | display: absolute; 10 | justify-content: center; 11 | align-items: center; 12 | min-height: 100vh; 13 | background: #000; 14 | } 15 | 16 | .login-mainContainer, 17 | .signup-mainContainer { 18 | position: absolute; 19 | width: 100vw; 20 | height: 100vh; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | gap: 2px; 25 | flex-wrap: wrap; 26 | overflow: hidden; 27 | } 28 | 29 | .login-mainContainer::before, 30 | .signup-mainContainer::before { 31 | content: ''; 32 | position: absolute; 33 | width: 100%; 34 | height: 100%; 35 | background: linear-gradient($red, $yellow, $green); 36 | animation: animate 5s linear infinite; 37 | } 38 | 39 | //Animating the background 40 | @keyframes animate { 41 | 0% { 42 | transform: translateY(-100%); 43 | } 44 | 100% { 45 | transform: translateY(100%); 46 | } 47 | } 48 | 49 | //Color-changing spans. 50 | .login-mainContainer span, 51 | .signup-mainContainer span { 52 | position: relative; 53 | display: block; 54 | width: calc(6.25vw - 2px); 55 | height: calc(6.25vw - 2px); 56 | background: #181818; 57 | z-index: 2; 58 | transition: 1.5s; 59 | } 60 | .login-mainContainer span:hover, 61 | .signup-mainContainer span:hover { 62 | background: $green; 63 | transition: 0s; 64 | } 65 | 66 | .login-mainContainer .login-rightContainer, 67 | .signup-mainContainer .signup-box { 68 | position: absolute; 69 | width: 400px; 70 | background: #222; 71 | z-index: 1000; 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | padding: 40px; 76 | border-radius: 4px; 77 | box-shadow: 0 15px 35px rgba(0, 0, 0, 9); 78 | } 79 | 80 | //Login content positioning. 81 | .login-mainContainer .login-rightContainer .content, 82 | .signup-mainContainer .signup-box .content { 83 | position: relative; 84 | width: 100%; 85 | display: flex; 86 | justify-content: center; 87 | align-items: center; 88 | flex-direction: column; 89 | gap: 40px; 90 | } 91 | .login-mainContainer .login-rightContainer .content, 92 | .signup-mainContainer .signup-box .content { 93 | position: relative; 94 | width: 100%; 95 | display: flex; 96 | justify-content: center; 97 | align-items: center; 98 | flex-direction: column; 99 | gap: 40px; 100 | } 101 | .login-mainContainer .login-rightContainer .content h1, 102 | .signup-mainContainer .signup-box .content h1 { 103 | font-size: 2em; 104 | color: $green; 105 | text-transform: uppercase; 106 | } 107 | .signup-mainContainer .signup-box .content h2 { 108 | font-size: 1.5em; 109 | color: $green; 110 | } 111 | 112 | //Login form positioning and styling. 113 | .login-mainContainer .login-rightContainer .content .form, 114 | .signup-mainContainer .signup-box .content .form { 115 | width: 100%; 116 | display: flex; 117 | flex-direction: column; 118 | gap: 25px; 119 | } 120 | .login-mainContainer .login-rightContainer .content .form .inputBox, 121 | .signup-mainContainer .signup-box .content .form .inputBox { 122 | position: relative; 123 | width: 100%; 124 | margin-bottom: 20px; 125 | } 126 | .login-mainContainer .login-rightContainer .content .form .inputBox input, 127 | .signup-mainContainer .signup-box .content .form .inputBox input { 128 | position: relative; 129 | width: 100%; 130 | background: #333; 131 | border: none; 132 | outline: none; 133 | padding: 25px 10px 7.5px; 134 | border-radius: 4px; 135 | color: #fff; 136 | font-weight: 500; 137 | font-size: 1em; 138 | } 139 | .login-mainContainer .login-rightContainer .content .form .inputBox i, 140 | .signup-mainContainer .signup-box .content .form .inputBox i { 141 | position: absolute; 142 | left: 0; 143 | padding: 15px 10px; 144 | font-style: normal; 145 | color: #aaa; 146 | transition: 0.5s; 147 | pointer-events: none; 148 | } 149 | .signin .content .form .inputBox input:focus ~ i, 150 | .signin .content .form .inputBox input:valid ~ i { 151 | transform: translateY(-7.5px); 152 | font-size: 0.8em; 153 | color: $green; 154 | } 155 | 156 | //Create Account Redirecting 157 | .create-account-redirect-link { 158 | display: flex; 159 | font-size: 14px; 160 | justify-content: space-between; 161 | margin-bottom: 20px; 162 | } 163 | .create-account-redirect-link .question { 164 | color: white; 165 | } 166 | .create-account-redirect-link .answer { 167 | color: $green; 168 | cursor: pointer; 169 | text-decoration: none; 170 | } 171 | .create-account-redirect-link .answer:hover { 172 | opacity: 0.8; 173 | } 174 | .create-account-redirect-link .answer:active { 175 | opacity: 0.6; 176 | } 177 | 178 | //Login Button 179 | .login-button, 180 | .signup-button { 181 | width: 100%; 182 | border-style: none; 183 | border-radius: 4px; 184 | background: $green; 185 | color: #000; 186 | font-weight: 600; 187 | font-size: 1.35em; 188 | letter-spacing: 0.05em; 189 | padding-top: 10px; 190 | padding-bottom: 10px; 191 | cursor: pointer; 192 | } 193 | .login-button:hover, 194 | .signup-button:hover { 195 | opacity: 0.8; 196 | } 197 | .login-button:active, 198 | .signup-button:active { 199 | opacity: 0.6; 200 | } 201 | 202 | //Set the span width and heights based on media size. 203 | @media (max-width: 900px) { 204 | .login-mainContainer span { 205 | width: calc(10vw - 2px); 206 | height: calc(10vw - 2px); 207 | } 208 | } 209 | @media (max-width: 600px) { 210 | .login-mainContainer span { 211 | width: calc(20vw - 2px); 212 | height: calc(20vw - 2px); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /__tests__/signup.test.js: -------------------------------------------------------------------------------- 1 | // Imports to run tests 2 | import React from 'react'; 3 | import { render, screen, fireEvent } from '@testing-library/react'; 4 | import { MemoryRouter } from 'react-router-dom'; 5 | // history package provides a way to manage session history in JS environments 6 | // history package is commonly incuded as a dependency when using React routing libraries like 'react-router-dom' 7 | // history is a transitive dependency in react-router-dom (aka do not need to install separately) 8 | import { createMemoryHistory } from 'history'; 9 | // **note**: will need to import signup page/functionality 10 | 11 | // render sign up page 12 | function renderSignUpPage() { 13 | render( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | describe('signUpPage', () => { 21 | test('renders sign-up form elements', () => { 22 | // before each test, render the sign up page (virtual environment) 23 | beforeEach(() => { 24 | renderSignUpPage(); 25 | }); 26 | 27 | // query virtual DOM to select elements 28 | // assertions 29 | expect(screen.getByLabelText('Name')).toBeInTheDocument(); 30 | expect(screen.getByLabelText('Username')).toBeInTheDocument(); 31 | expect(screen.getByLabelText('Password')).toBeInTheDocument(); 32 | expect(screen.getByRole('button', { name: 'Sign Up' })).toBeInTheDocument(); 33 | }); 34 | 35 | //test: is an error message appearing if un or pw is missing? 36 | test('display error if all input fields not filled', () => { 37 | const submitButton = screen.getByRole('button', { name: 'Sign Up' }); 38 | // simulate submit form funcitonality 39 | fireEvent.click(submitButton); 40 | 41 | // query virtual DOM to find element that has id set to error 42 | const errorMessage = screen.getByTestId('error'); 43 | // assertions 44 | expect(errorMessage).toBeInTheDocument(); 45 | expect(errorMessage.textContent).toBe( 46 | 'All input fields are required to be filled on form submission.' 47 | ); 48 | }); 49 | 50 | test('error message is removed when form submit is successful', async () => { 51 | // querying virtual DOM to select elements 52 | const userInput = screen.getByLabelText('Username'); 53 | const passwordInput = screen.getByLabelText('Password'); 54 | const nameInput = screen.getByLabelText('Name'); 55 | const submitButton = screen.getByRole('button', { name: 'Sign Up' }); 56 | 57 | // simulate user input in each of the input fields setting their values 58 | fireEvent.change(nameInput, { target: { value: 'testname' } }); 59 | fireEvent.change(userInput, { target: { value: 'testuser' } }); 60 | fireEvent.change(passwordInput, { target: { value: 'testpass' } }); 61 | 62 | // simulate form submit 63 | fireEvent.click(submitButton); 64 | 65 | // wait for promise resolution 66 | await Promise.resolve(); 67 | 68 | // look for the element with id set to error message 69 | const errorMessage = screen.queryByTestId('error-message'); 70 | 71 | // assertion: form submission is successful, so the error message should not be present 72 | expect(errorMessage).toBeNull(); 73 | }); 74 | 75 | test('display a success message and redirects to dashboard (homepage) after form submits successfully', async () => { 76 | // create a mock form submission function that resolves with a success response 77 | const mockFormSubmit = jest.fn(() => Promise.resolve({ success: true })); 78 | 79 | // create a history object to simulate navigation 80 | // this function creates a mock version of the navigation history, specifically for testing purposes 81 | const history = createMemoryHistory(); 82 | 83 | // render the signuppage component within the MemoryRouter with the history 84 | render( 85 | // pass down history as a prop to mimic how the application would handle navigation in the browser 86 | // this allows us to validate that the sign-up form redirects the user to the homepage without needing to deal with a real browser or affect the actual URL 87 | 88 | 89 | 90 | ); 91 | 92 | // get elements from the rendered signuppage component 93 | const nameInput = screen.getByLabelText('Name'); 94 | const userInput = screen.getByLabelText('Username'); 95 | const passwordInput = screen.getByLabelText('Password'); 96 | const submitButton = screen.getByRole('button', { name: 'Sign Up' }); 97 | 98 | // simulate change events on form input fields to update their values 99 | fireEvent.change(nameInput, { 100 | target: { name: 'name', value: 'testname' }, 101 | }); 102 | fireEvent.change(userInput, { 103 | target: { name: 'username', value: 'testuser' }, 104 | }); 105 | fireEvent.change(passwordInput, { 106 | target: { name: 'password', value: 'testpassword' }, 107 | }); 108 | 109 | // simulate click event triggers form submission 110 | fireEvent.click(submitButton); 111 | 112 | // check if the success message is displayed after form submission 113 | const successMessage = screen.getByTestId('success message'); 114 | expect(successMessage).toBeInTheDocument(); 115 | 116 | // assertion: the mockFormSubmit should be called with the correct form data 117 | expect(mockFormSubmit).toBeCalledTimes(1); 118 | expect(mockFormSubmit).toBeCalledWith({ 119 | name: 'testname', 120 | username: 'testuser', 121 | password: 'testpassword', 122 | }); 123 | 124 | // check if the user is redirected to the dashboard (homepage) after form submission 125 | expect(history.location.pathname).toBe('/homepage'); 126 | }); 127 | }); 128 | 129 | // NOTES: 130 | // libraries : jest, react testing library, react router DOM 131 | // react router dom: will handle routing and navigation between different pages or views 132 | // use react testing library: provides virtual DOM environment to render and interact with components during testing 133 | // describe: used to create test suite in our case Sign Up page unit tests live within describe block 134 | // run tests by command: npm jest fileName 135 | -------------------------------------------------------------------------------- /server/controllers/installController.js: -------------------------------------------------------------------------------- 1 | // import { spawn, spawnSync } from 'child_process'; 2 | const { spawn, spawnSync } = require('child_process'); 3 | 4 | const installController = { 5 | installPrometheus: (req, res, next) => { 6 | //Add the Prometheus repository to Helm. 7 | const addRepo = spawnSync( 8 | 'helm repo add prometheus-community https://prometheus-community.github.io/helm-charts', 9 | { stdio: 'inherit', shell: true } 10 | ); 11 | 12 | if (addRepo.stderr) { 13 | console.log( 14 | `helm repo add prometheus community error: ${addRepo.stderr}` 15 | ); 16 | return next({ 17 | log: 'Error on installPrometheus middleware.', 18 | status: 500, 19 | message: { 20 | err: 'An error occurred when trying to add the prometheus-community helm charts repo.', 21 | }, 22 | }); 23 | } 24 | 25 | //Update the prometheus-community repository. 26 | const updateRepo = spawnSync('helm repo update', { 27 | stdio: 'inherit', 28 | shell: true, 29 | }); 30 | 31 | if (updateRepo.stderr) { 32 | console.log(`helm repo update error: ${updateRepo.stderr}`); 33 | return next({ 34 | log: 'Error on installPrometheus middleware.', 35 | status: 500, 36 | message: { 37 | err: 'An error occurred when trying to update Helm repositories.', 38 | }, 39 | }); 40 | } 41 | 42 | //Install the kube-prometheus-stack Helm chart from the prometheus-community repository. 43 | //I'm not sure why we would need this if we have our own dashboard. 44 | const installKubePromStack = spawnSync( 45 | 'helm install prometheus prometheus-community/kube-prometheus-stack', 46 | { stdio: 'inherit', shell: true } 47 | ); 48 | 49 | if (installKubePromStack.stderr) { 50 | console.log( 51 | `helm install prometheus prometheus-community/kube-prometheus-stack error: ${installKubePromStack.stderr}` 52 | ); 53 | return next({ 54 | log: 'Error on installPrometheus middleware.', 55 | status: 500, 56 | message: { 57 | err: 'An error occurred when trying to install Prometheus.', 58 | }, 59 | }); 60 | } 61 | 62 | return next(); 63 | }, 64 | 65 | recreatePromGraf: (req, res, next) => { 66 | console.log('We entered the recreatePromGraf in the Install Controller'); 67 | 68 | const deleteConfigmap = spawnSync( 69 | 'kubectl delete configmap prometheus-grafana', 70 | { stdio: 'inherit', shell: true } 71 | ); 72 | 73 | if (deleteConfigmap.stderr) { 74 | console.log( 75 | `deleting prometheus-grafana configmap error: ${deleteConfigmap.stderr}` 76 | ); 77 | return next({ 78 | log: 'Error on recreatePromGraf middleware.', 79 | status: 500, 80 | message: { 81 | err: 'An error occurred when trying to delete the prometheus-grafana Configmap.', 82 | }, 83 | }); 84 | } 85 | 86 | // Applying custom yaml file 87 | const applyPromGrafYaml = spawnSync( 88 | 'kubectl apply -f prometheus-grafana.yaml', 89 | { stdio: 'inherit', shell: true } 90 | ); 91 | 92 | if (applyPromGrafYaml.stderr) { 93 | console.log( 94 | `creating new prometheus-grafana configmap error: ${applyPromGrafYaml.stderr}` 95 | ); 96 | return next({ 97 | log: 'Error on recreatePromGraf middleware. An error occurred when trying to create a new configmap with prometheus-grafana.yaml', 98 | status: 500, 99 | message: { 100 | err: 'An error occurred when trying to create a new configmap with prometheus-grafana.yaml.', 101 | }, 102 | }); 103 | } 104 | 105 | // Get a list of pods to find the full prometheus-grafana pod name, since this name varies by user. 106 | const kubectlGetPods = spawnSync('kubectl', ['get', 'pods'], { 107 | encoding: 'utf-8', 108 | }); 109 | 110 | if (kubectlGetPods.stderr) { 111 | console.log(`kubectl get pods error: ${kubectlGetPods.stderr} `); 112 | return next({ 113 | log: 'Error on recreatePromGraf middleware. An error occurred when trying get a list of kubernetes pods', 114 | status: 500, 115 | message: { 116 | err: 'An error occurred when trying get a list of kubernetes pods.', 117 | }, 118 | }); 119 | } 120 | 121 | //In an array, separate each line of the result of running "kubectl run pods". 122 | const kubectlGetPodsText = kubectlGetPods.stdout.split('\n'); 123 | 124 | //Find the pod that contains "prometheus-grafana". 125 | let pod; 126 | kubectlGetPodsText.forEach((line) => { 127 | if (line.includes('prometheus-grafana')) pod = line.split(' ')[0]; 128 | }); 129 | 130 | // Delete old prometheus-grafana pod 131 | const deletePromGrafPod = spawnSync(`kubectl delete pod ${pod}`, { 132 | stdio: 'inherit', 133 | shell: true, 134 | }); 135 | 136 | if (deletePromGrafPod.stderr) { 137 | console.log(`deleting old pod error: ${deletePromGrafPod.stderr}`); 138 | return next({ 139 | log: 'Error on recreatePromGraf middleware. An error occurred when trying to delete the old pod.', 140 | status: 500, 141 | message: { 142 | err: 'An error occurred when trying to delete the old pod.', 143 | }, 144 | }); 145 | } 146 | 147 | return next(); 148 | }, 149 | 150 | portForward: (req, res, next) => { 151 | //Declare a port to independently display and access Grafana. 152 | const PORT = 3000; 153 | 154 | //Asyncronously forward prometheus-grafana to the PORT. 155 | const portFw = spawn( 156 | `kubectl port-forward deployment/prometheus-grafana ${PORT}`, 157 | { shell: true } 158 | ); 159 | 160 | //When the process has started, move on to the next middleware. 161 | portFw.on('spawn', () => { 162 | console.log( 163 | `The process of port forwarding to ${PORT} has started successfully.` 164 | ); 165 | return next(); 166 | }); 167 | 168 | //If there's an error, 169 | portFw.stderr.on('data', (data) => { 170 | console.log( 171 | `port forwarding prometheus-grafana to PORT ${PORT} \n error: ${portFw.stderr} \n data: ${data}` 172 | ); 173 | return next({ 174 | log: 'Error on portForward middleware.', 175 | status: 500, 176 | message: { 177 | err: `An error occurred when trying to forward the prometheus-grafana port to PORT ${PORT}.`, 178 | }, 179 | }); 180 | }); 181 | 182 | return next(); 183 | }, 184 | }; 185 | 186 | module.exports = installController; 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![kubeready](https://github.com/oslabs-beta/kubeready/assets/133065870/945e8dc5-6d2c-42e5-b93f-64271ff79548) 2 | 3 | # Technology Stack 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | # Introduction 25 | For those experienced with the Kubernetes environment, the intricacies of monitoring cluster health are well understood. Navigating the complexities inherent in visualizing a cluster is a challenge that necessitates attention to a multitude of factors. The challenge lies in discerning which metrics hold critical significance and which ones might be relegated to a less prominent role. Get ready to be ready with kubeready! With our tool, visualizing kubernetes metrics from any local cluster becomes simple. Crafted with a developer-centric approach, kubeready boasts a user-friendly platform, delivering real-time metric visualization. By offering an instantaneous display of these metrics, kubeready empowers developers to promptly address performance issues as they emerge, significantly elevating their responsiveness and efficacy in tackling such challenges. 26 | 27 | # Features 28 | ## User-facing features 29 | The kubeready tool is designed to streamline and enhance your experience with Kubernetes cluster management, monitoring, and analysis. kubeready was built with comprehensive pre-built metrics, automated dependency installation, seamless Grafana and Prometheus integration, custom dashboard generation, password encryption, and a comprehensive developer-side testing suite. 30 | * Connect your Kubernetes clusters to pre-built metrics, allowing you to visualize detailed CPU, Memory, Network, and Disk metrics. 31 | * Automated dependency installation, running commands upon account creation to connect Helm charts and custom Grafana configurations. 32 | * Automated Grafana dashboard creation, rendering a new dashboard specific to the user's machine. 33 | * Automated connection to Prometheus, scraping metrics more quickly without additional user setup. 34 | * Security is a top priority in any Kubernetes environment. kubeready provides password encryption through bcrypt. 35 | ## Developer-facing features 36 | * Test-driven development: kubeready's comprehensive testing suite (using Jest and React Testing Library) enables future developers to assess code functionality throughout the frontend and backend of the application. 37 | 38 | # Requirements 39 | Please free up the following ports: 40 | Technology | Port Number 41 | ------------- | ------------- 42 | Grafana | 3000 43 | server | 3001 44 | kubeready | 8080 45 | 46 | # Installation/Getting Started 47 | ## To start a new Kubernetes cluster 48 | 1. Install [Docker](https://www.docker.com/products/docker-desktop/) on your OS so a container can be spun up. 49 | 2. Begin running a minikube to create a kubernetes cluster by running the command 50 | ``` 51 | minikube start 52 | ``` 53 | ## To run kubeready 54 | 1. Install [Helm](https://helm.sh/docs/intro/install/). 55 | 2. Clone this respository onto your local machine. 56 | 3. Run the following commands in your local repository directory. 57 | 58 | ``` 59 | npm install 60 | ``` 61 | ``` 62 | npm run build 63 | ``` 64 | ``` 65 | npm run start 66 | ``` 67 | 4. Open localhost:3001 68 | ``` 69 | https://localhost:3001/ 70 | ``` 71 | 5. Create an account or sign in. 72 | ![LoginToDashboardGif](https://github.com/oslabs-beta/kubeready/blob/njpallivathucal-readMe/kubeready%20login%20gif.gif?raw=true) 73 | ![SignUpGif](https://github.com/oslabs-beta/kubeready/blob/njpallivathucal-readMe/kubeready%20signup.gif?raw=true) 74 | 75 | # RoadMap Regarding Present/Future Considerations 76 | 💯 = Ready to use
77 | 👨‍💻 = In progress
78 | 🙏🏻 = Looking for contributors
79 | Feature | Status 80 | ------------- | ------------- 81 | Seamless grafana integration | 💯 82 | Seamless prometheus integration | 💯 83 | Password encryption | 💯 84 | Addition of testing suite | 💯 85 | Custom dashboard creation and render | 💯 86 | Automate login proccess using CLI for grafana | 👨‍💻 87 | Implement typescript conversion | 🙏🏻 88 | Add functionality to monitor health of invididual pods | 🙏🏻 89 | Add an overall health score for each metric to allow for more immediate response by developer when metric dips to critical level | 🙏🏻 90 | Implement typescript conversion for codebase | 🙏🏻 91 | Addition of a notification/alert system when metrics dip to critical | 🙏🏻 92 | 93 | # Publications 94 | Read our Medium Article [Here](https://medium.com/@kubeready/introducing-kubeready-ea51e8e705ee)! 95 | 96 | # Meet the Team 97 | 98 | 99 | 102 | 105 | 108 | 111 | 114 | 115 |
100 | Alana Herlands
Alana Herlands
GitHub | LinkedIn
Software Engineer 101 |
103 | Diane Moon
Diane Moon
GitHub | LinkedIn
Software Engineer 104 |
106 | Serena Romano
Serena Romano
GitHub | LinkedIn
Software Engineer 107 |
109 | Alvin Cheung
Alvin Cheung
GitHub | LinkedIn
Software Engineer 110 |
112 | Noel Pallivathucal
Noel Pallivathucal
GitHub | LinkedIn
Software Engineer 113 |
116 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import kubereadylogo_transparent from '../assets/kubereadylogo_transparent.jpg'; 4 | 5 | const Login = () => { 6 | const [username, setUsername] = useState(''); 7 | const [password, setPassword] = useState(''); 8 | const [isLoading, setIsLoading] = useState(false); 9 | const navigate = useNavigate(); 10 | 11 | // form handleChanges - grabs data from username/password forms and adds them to state 12 | const handleUsernameChange = (event) => { 13 | setUsername(event.target.value); 14 | }; 15 | 16 | const handlePasswordChange = (event) => { 17 | setPassword(event.target.value); 18 | }; 19 | 20 | // on login click 21 | const handleSubmit = (e) => { 22 | e.preventDefault(); 23 | fetch('api/login', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | body: JSON.stringify({ 29 | username, 30 | password, 31 | }), 32 | }) 33 | .then((response) => { 34 | if (!response.ok) { 35 | throw new Error('Could not login'); 36 | } 37 | return response.json(); 38 | }) 39 | //IF SUCCESSFUL, redirect to dashboard 40 | .then((loggedInUser) => { 41 | console.log('User has logged in'); 42 | setIsLoading(false); 43 | navigate('/homepage'); 44 | }) 45 | .catch((error) => { 46 | setIsLoading(false); 47 | console.log('User couldnt log in'); 48 | }); 49 | }; 50 | 51 | return ( 52 |
53 | {/*
*/} 54 |
55 | {' '} 56 | {' '} 57 | {' '} 58 | {' '} 59 | {' '} 60 | {' '} 61 | {' '} 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 | {' '} 90 | {' '} 91 | {' '} 92 | {' '} 93 | {' '} 94 | {' '} 95 | {' '} 96 | {' '} 97 | {' '} 98 | {' '} 99 | {' '} 100 | {' '} 101 | {' '} 102 | {' '} 103 | {' '} 104 | {' '} 105 | {' '} 106 | 107 | {/*
108 |
109 | Are you ready? 110 |

Before logging in:

111 |
    112 |
  1. Please install Helm
  2. 113 |
  3. Free up Port 3000
  4. 114 |
115 | 118 |
119 |
*/} 120 |
121 |
122 | transparent_logo 128 |

Get started

129 |
130 |
131 |
132 | 140 |
141 |
142 | 150 |
151 |
152 | 153 | Create an account 154 | 155 |
156 | 163 |
164 |
165 |
166 |
167 |
168 |
169 | ); 170 | }; 171 | 172 | export default Login; 173 | -------------------------------------------------------------------------------- /client/components/SignUp.jsx: -------------------------------------------------------------------------------- 1 | // Import dependencies 2 | import React, { useState } from 'react'; 3 | import { Link, useNavigate } from 'react-router-dom'; 4 | import kubereadylogo_transparent from '../assets/kubereadylogo_transparent.jpg'; 5 | 6 | const SignUp = () => { 7 | const [name, setName] = useState(''); 8 | const [email, setEmail] = useState(''); 9 | const [username, setUsername] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | 12 | const [isLoading, setIsLoading] = useState(false); 13 | const navigate = useNavigate(); 14 | 15 | // form handleChanges 16 | const handleNameChange = (event) => { 17 | setName(event.target.value); 18 | }; 19 | 20 | const handleEmailChange = (event) => { 21 | setEmail(event.target.value); 22 | }; 23 | 24 | const handleUsernameChange = (event) => { 25 | setUsername(event.target.value); 26 | }; 27 | 28 | const handlePasswordChange = (event) => { 29 | setPassword(event.target.value); 30 | }; 31 | 32 | // on signup click 33 | const handleSubmit = (e) => { 34 | setIsLoading(true); 35 | e.preventDefault(); 36 | // grab data out of state and post to mongoDB 37 | fetch('/api/signup', { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: JSON.stringify({ name, email, username, password }), 43 | }) 44 | .then((response) => { 45 | if (!response.ok) { 46 | throw new Error('User was not created - response error'); 47 | } 48 | return response.json(); 49 | }) 50 | // If successful, send user to dashboard page 51 | .then((user) => { 52 | setIsLoading(false); 53 | navigate('/homepage'); 54 | }) 55 | .catch((error) => { 56 | setIsLoading(false); 57 | }); 58 | }; 59 | 60 | return ( 61 |
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 | {' '} 90 | {' '} 91 | {' '} 92 | {' '} 93 | {' '} 94 | {' '} 95 | {' '} 96 | {' '} 97 | {' '} 98 | {' '} 99 | {' '} 100 | {' '} 101 | {' '} 102 | {' '} 103 | {' '} 104 | {' '} 105 | {' '} 106 | {' '} 107 | {' '} 108 | {' '} 109 | {' '} 110 | {' '} 111 | {' '} 112 | {' '} 113 | 114 |
115 |
116 | transparent_logo 122 |

Manage & monitor your K8 cluster metrics with ease

123 |
124 |
125 |
126 | 134 |
135 |
136 | 144 |
145 |
146 | 154 |
155 |
156 | 164 |
165 |
166 | {/*

Have an account?

*/} 167 | 168 | Already have an account? 169 | 170 |
171 | 178 |
179 |
180 |
181 |
182 |
183 | ); 184 | }; 185 | 186 | export default SignUp; 187 | -------------------------------------------------------------------------------- /client/styles/_oldlogin.scss: -------------------------------------------------------------------------------- 1 | // NOT USING THIS CURRENTLY NOW 2 | 3 | // .login { 4 | // font-family: 'Roboto', sans-serif; 5 | // text-align: center; 6 | // font-size: x-large; 7 | // } 8 | // 9 | // .login-mainContainer{ 10 | // // font-family: 'Roboto', sans-serif; 11 | // // display: flex; 12 | // // justify-content: center; 13 | // // align-items: stretch; 14 | // // flex-wrap: wrap; 15 | // // height: 100vh; 16 | // // margin: 0 auto; 17 | // // max-width: 1200px; 18 | // position: absolute; 19 | // width: 100vw; 20 | // height: 100vh; 21 | // display: flex; 22 | // justify-content: center; 23 | // align-items: center; 24 | // gap: 2px; 25 | // flex-wrap: wrap; 26 | // overflow: hidden; 27 | 28 | // .leftContainer, .rightContainer { 29 | // flex: 1; 30 | // padding: 20px; 31 | // border-radius: 10px; 32 | // transition: transform 0.3s ease; 33 | // max-width: calc(50% - 20px); 34 | // min-width: 300px; 35 | // max-width: 100%; 36 | // margin-top: calc((100vh - 60vh) / 2); 37 | // } 38 | 39 | // .leftContainer { 40 | // margin-right: 10px; 41 | // height: 50vh; 42 | 43 | // // // box-shadow: 0px; 44 | // // flex: 1; 45 | // // top: 50%; 46 | // // // left: 200px; // commenting out just for testing 47 | // // // transform: translate(0, -50%); // commenting out just for testing 48 | // // // background: linear-gradient(135deg, #e6f7ff, #cceeff); 49 | // // padding: 20px; 50 | // // // box-shadow: 0px, 4px, 8px rgba(0, 0, 0, 0.1); 51 | // // border-radius: 10px; 52 | // // // position: fixed; // commenting out just for testing 53 | // // justify-content: center; 54 | // // align-items: center; 55 | // // transition: transform 0.3s ease; 56 | // // flex-wrap: wrap; 57 | // // max-width: calc(50% - 20px); 58 | // // min-width: 300px; 59 | // // height: 50vh; 60 | // // // width: 100vh; 61 | 62 | // .logoImage { 63 | // object-fit: contain; 64 | // width: 90%; 65 | // height: 20%; 66 | // } 67 | 68 | // #instructions-title { 69 | // color: black; 70 | // font: sans-serif; 71 | // } 72 | 73 | // #redirect-button { 74 | // border-radius: 20px; 75 | // } 76 | // } 77 | 78 | // .rightContainer { 79 | // background: linear-gradient(135deg, #fafcfd, #30adeb); 80 | // box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); 81 | // margin-left: 10px; /* Add a margin to separate the two containers */ 82 | // height: 350px; 83 | // // justify-content: center; 84 | // // align-items: center; 85 | // // flex: 1; 86 | // // // box-shadow: 0px; 87 | // // top: 50%; 88 | // // right: 200px; // commenting out just for testing 89 | // // padding: 50px; 90 | // // // transform: translate(0, -50%); // commenting out just for testing 91 | // // background: linear-gradient(135deg, #fafcfd, #30adeb); 92 | // // padding: 20px; 93 | // // box-shadow: 0px, 4px, 8px rgba(0, 0, 0, 0.1); 94 | // // border-radius: 10px; 95 | // // // position: fixed; // commenting out just for testing 96 | // // transition: transform 0.3s ease; 97 | // // max-width: calc(50% - 20px); 98 | // // // min-width: 300px; 99 | // // width: 250px; // commenting out just for testing 100 | // // height: 350px; 101 | // // flex-wrap: wrap 102 | 103 | // //Button to create account. 104 | // .createAcct-button{ 105 | // color: white; 106 | // background-color: rgb(8, 8, 8); 107 | // border-radius: 20px; 108 | // padding: 10px 20px; 109 | // margin-right: 5px; 110 | // cursor: pointer; 111 | // a { 112 | // color: whitesmoke; // Replace with the color you want 113 | // } 114 | // } 115 | 116 | // .login-button{ 117 | // color: white; 118 | // background-color: rgb(13, 13, 13); 119 | // border-radius: 20px; 120 | // padding: 10px 20px; 121 | // margin-right: 5px; 122 | // cursor: pointer; 123 | // } 124 | 125 | // //Input fields 126 | // .login-input{ 127 | // background-color: rgb(255, 252, 252); 128 | // border-radius: 8px; 129 | // padding: 10px; 130 | // margin-bottom: 10px; 131 | // color: darkblue; 132 | // width: 200px; 133 | // font-size: 16px; 134 | // } 135 | // } 136 | // } 137 | // * 138 | // { 139 | // margin: 0; 140 | // padding: 0; 141 | // box-sizing: border-box; 142 | // font-family: 'Quicksand', sans-serif; 143 | // } 144 | // body 145 | // { 146 | // display: flex; 147 | // justify-content: center; 148 | // align-items: center; 149 | // min-height: 100vh; 150 | // background: #000; 151 | // } 152 | // section 153 | // { 154 | // position: absolute; 155 | // width: 100vw; 156 | // height: 100vh; 157 | // display: flex; 158 | // justify-content: center; 159 | // align-items: center; 160 | // gap: 2px; 161 | // flex-wrap: wrap; 162 | // overflow: hidden; 163 | // } 164 | // section::before 165 | // { 166 | // content: ''; 167 | // position: absolute; 168 | // width: 100%; 169 | // height: 100%; 170 | // background: linear-gradient(#000,#0f0,#000); 171 | // animation: animate 5s linear infinite; 172 | // } 173 | // @keyframes animate 174 | // { 175 | // 0% 176 | // { 177 | // transform: translateY(-100%); 178 | // } 179 | // 100% 180 | // { 181 | // transform: translateY(100%); 182 | // } 183 | // } 184 | // section span 185 | // { 186 | // position: relative; 187 | // display: block; 188 | // width: calc(6.25vw - 2px); 189 | // height: calc(6.25vw - 2px); 190 | // background: #181818; 191 | // z-index: 2; 192 | // transition: 1.5s; 193 | // } 194 | // section span:hover 195 | // { 196 | // background: linear-gradient(135deg, $red, $green, $yellow); 197 | // transition: 0s; 198 | // } 199 | 200 | // section .signin 201 | // { 202 | // position: absolute; 203 | // width: 400px; 204 | // background: #222; 205 | // z-index: 1000; 206 | // display: flex; 207 | // justify-content: center; 208 | // align-items: center; 209 | // padding: 40px; 210 | // border-radius: 4px; 211 | // box-shadow: 0 15px 35px rgba(0,0,0,9); 212 | // } 213 | // section .signin .content 214 | // { 215 | // position: relative; 216 | // width: 100%; 217 | // display: flex; 218 | // justify-content: center; 219 | // align-items: center; 220 | // flex-direction: column; 221 | // gap: 40px; 222 | // } 223 | // section .signin .content h2 224 | // { 225 | // font-size: 2em; 226 | // color: #0f0; 227 | // text-transform: uppercase; 228 | // } 229 | // section .signin .content .form 230 | // { 231 | // width: 100%; 232 | // display: flex; 233 | // flex-direction: column; 234 | // gap: 25px; 235 | // } 236 | // section .signin .content .form .inputBox 237 | // { 238 | // position: relative; 239 | // width: 100%; 240 | // } 241 | // section .signin .content .form .inputBox input 242 | // { 243 | // position: relative; 244 | // width: 100%; 245 | // background: #333; 246 | // border: none; 247 | // outline: none; 248 | // padding: 25px 10px 7.5px; 249 | // border-radius: 4px; 250 | // color: #fff; 251 | // font-weight: 500; 252 | // font-size: 1em; 253 | // } 254 | // section .signin .content .form .inputBox i 255 | // { 256 | // position: absolute; 257 | // left: 0; 258 | // padding: 15px 10px; 259 | // font-style: normal; 260 | // color: #aaa; 261 | // transition: 0.5s; 262 | // pointer-events: none; 263 | // } 264 | // .signin .content .form .inputBox input:focus ~ i, 265 | // .signin .content .form .inputBox input:valid ~ i 266 | // { 267 | // transform: translateY(-7.5px); 268 | // font-size: 0.8em; 269 | // color: #fff; 270 | // } 271 | // .signin .content .form .links 272 | // { 273 | // position: relative; 274 | // width: 100%; 275 | // display: flex; 276 | // justify-content: space-between; 277 | // } 278 | // .signin .content .form .links a 279 | // { 280 | // color: #fff; 281 | // text-decoration: none; 282 | // } 283 | // .signin .content .form .links a:nth-child(2) 284 | // { 285 | // color: #0f0; 286 | // font-weight: 600; 287 | // } 288 | // .signin .content .form .inputBox input[type="submit"] 289 | // { 290 | // padding: 10px; 291 | // background: #0f0; 292 | // color: #000; 293 | // font-weight: 600; 294 | // font-size: 1.35em; 295 | // letter-spacing: 0.05em; 296 | // cursor: pointer; 297 | // } 298 | // input[type="submit"]:active 299 | // { 300 | // opacity: 0.6; 301 | // } 302 | // @media (max-width: 900px) 303 | // { 304 | // section span 305 | // { 306 | // width: calc(10vw - 2px); 307 | // height: calc(10vw - 2px); 308 | // } 309 | // } 310 | // @media (max-width: 600px) 311 | // { 312 | // section span 313 | // { 314 | // width: calc(20vw - 2px); 315 | // height: calc(20vw - 2px); 316 | // } 317 | // } 318 | -------------------------------------------------------------------------------- /server/grafana/panels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "collapsed": false, 4 | "gridPos": { 5 | "h": 1, 6 | "w": 24, 7 | "x": 0, 8 | "y": 0 9 | }, 10 | "id": 8, 11 | "panels": [], 12 | "title": "Overview", 13 | "type": "row" 14 | }, 15 | { 16 | "datasource": { 17 | "type": "prometheus", 18 | "uid": "${DS_PROMETHEUS}" 19 | }, 20 | "fieldConfig": { 21 | "defaults": { 22 | "color": { 23 | "mode": "continuous-GrYlRd" 24 | }, 25 | "mappings": [], 26 | "max": 1, 27 | "min": 0, 28 | "thresholds": { 29 | "mode": "absolute", 30 | "steps": [ 31 | { 32 | "color": "green", 33 | "value": null 34 | }, 35 | { 36 | "color": "red", 37 | "value": 80 38 | } 39 | ] 40 | }, 41 | "unit": "percentunit" 42 | }, 43 | "overrides": [] 44 | }, 45 | "gridPos": { 46 | "h": 8, 47 | "w": 11, 48 | "x": 0, 49 | "y": 1 50 | }, 51 | "id": 1, 52 | "options": { 53 | "displayMode": "lcd", 54 | "minVizHeight": 10, 55 | "minVizWidth": 0, 56 | "orientation": "horizontal", 57 | "reduceOptions": { 58 | "calcs": ["lastNotNull"], 59 | "fields": "", 60 | "values": false 61 | }, 62 | "showUnfilled": true, 63 | "valueMode": "color" 64 | }, 65 | "pluginVersion": "10.0.2", 66 | "targets": [ 67 | { 68 | "datasource": { 69 | "type": "prometheus", 70 | "uid": "${DS_PROMETHEUS}" 71 | }, 72 | "editorMode": "code", 73 | "exemplar": true, 74 | "expr": "avg(1-rate(node_cpu_seconds_total{mode=\"idle\"}[$__rate_interval]))", 75 | "instant": false, 76 | "interval": "", 77 | "legendFormat": "Real", 78 | "range": true, 79 | "refId": "A" 80 | }, 81 | { 82 | "datasource": { 83 | "type": "prometheus", 84 | "uid": "${DS_PROMETHEUS}" 85 | }, 86 | "editorMode": "code", 87 | "expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\"}) / sum(machine_cpu_cores)", 88 | "hide": false, 89 | "instant": false, 90 | "legendFormat": "Requests", 91 | "range": true, 92 | "refId": "B" 93 | }, 94 | { 95 | "datasource": { 96 | "type": "prometheus", 97 | "uid": "${DS_PROMETHEUS}" 98 | }, 99 | "editorMode": "code", 100 | "expr": "sum(kube_pod_container_resource_limits{resource=\"cpu\"}) / sum(machine_cpu_cores)", 101 | "hide": false, 102 | "instant": false, 103 | "legendFormat": "Limits", 104 | "range": true, 105 | "refId": "C" 106 | } 107 | ], 108 | "title": "Global CPU Usage", 109 | "type": "bargauge" 110 | }, 111 | { 112 | "datasource": { 113 | "type": "prometheus", 114 | "uid": "${DS_PROMETHEUS}" 115 | }, 116 | "description": "", 117 | "fieldConfig": { 118 | "defaults": { 119 | "color": { 120 | "mode": "continuous-GrYlRd" 121 | }, 122 | "custom": { 123 | "axisCenteredZero": false, 124 | "axisColorMode": "text", 125 | "axisLabel": "MEMORY", 126 | "axisPlacement": "auto", 127 | "barAlignment": 0, 128 | "drawStyle": "line", 129 | "fillOpacity": 10, 130 | "gradientMode": "scheme", 131 | "hideFrom": { 132 | "legend": false, 133 | "tooltip": false, 134 | "viz": false 135 | }, 136 | "lineInterpolation": "linear", 137 | "lineWidth": 2, 138 | "pointSize": 5, 139 | "scaleDistribution": { 140 | "type": "linear" 141 | }, 142 | "showPoints": "never", 143 | "spanNulls": false, 144 | "stacking": { 145 | "group": "A", 146 | "mode": "none" 147 | }, 148 | "thresholdsStyle": { 149 | "mode": "off" 150 | } 151 | }, 152 | "mappings": [], 153 | "thresholds": { 154 | "mode": "absolute", 155 | "steps": [ 156 | { 157 | "color": "green", 158 | "value": null 159 | }, 160 | { 161 | "color": "#EAB839", 162 | "value": 0.5 163 | }, 164 | { 165 | "color": "red", 166 | "value": 0.7 167 | } 168 | ] 169 | }, 170 | "unit": "percentunit" 171 | }, 172 | "overrides": [] 173 | }, 174 | "gridPos": { 175 | "h": 8, 176 | "w": 9, 177 | "x": 11, 178 | "y": 1 179 | }, 180 | "id": 6, 181 | "options": { 182 | "legend": { 183 | "calcs": [], 184 | "displayMode": "list", 185 | "placement": "bottom", 186 | "showLegend": false 187 | }, 188 | "tooltip": { 189 | "mode": "single", 190 | "sort": "none" 191 | } 192 | }, 193 | "targets": [ 194 | { 195 | "datasource": { 196 | "type": "prometheus", 197 | "uid": "${DS_PROMETHEUS}" 198 | }, 199 | "editorMode": "code", 200 | "exemplar": true, 201 | "expr": "sum(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes)", 202 | "instant": false, 203 | "legendFormat": "Memory Usage in %", 204 | "range": true, 205 | "refId": "A" 206 | } 207 | ], 208 | "title": "Cluster Memory Utilization", 209 | "type": "timeseries" 210 | }, 211 | { 212 | "datasource": { 213 | "type": "prometheus", 214 | "uid": "${DS_PROMETHEUS}" 215 | }, 216 | "fieldConfig": { 217 | "defaults": { 218 | "color": { 219 | "mode": "thresholds" 220 | }, 221 | "mappings": [], 222 | "thresholds": { 223 | "mode": "absolute", 224 | "steps": [ 225 | { 226 | "color": "blue", 227 | "value": null 228 | } 229 | ] 230 | } 231 | }, 232 | "overrides": [] 233 | }, 234 | "gridPos": { 235 | "h": 5, 236 | "w": 4, 237 | "x": 20, 238 | "y": 1 239 | }, 240 | "id": 4, 241 | "options": { 242 | "colorMode": "value", 243 | "graphMode": "none", 244 | "justifyMode": "auto", 245 | "orientation": "auto", 246 | "reduceOptions": { 247 | "calcs": ["lastNotNull"], 248 | "fields": "", 249 | "values": false 250 | }, 251 | "textMode": "auto" 252 | }, 253 | "pluginVersion": "10.0.2", 254 | "targets": [ 255 | { 256 | "datasource": { 257 | "type": "prometheus", 258 | "uid": "${DS_PROMETHEUS}" 259 | }, 260 | "editorMode": "code", 261 | "expr": "sum(kube_pod_status_phase{phase=\"Running\"})", 262 | "instant": false, 263 | "range": true, 264 | "refId": "A" 265 | } 266 | ], 267 | "title": "# of Running Pods", 268 | "type": "stat" 269 | }, 270 | { 271 | "datasource": { 272 | "type": "prometheus", 273 | "uid": "${DS_PROMETHEUS}" 274 | }, 275 | "fieldConfig": { 276 | "defaults": { 277 | "color": { 278 | "mode": "thresholds" 279 | }, 280 | "mappings": [], 281 | "thresholds": { 282 | "mode": "absolute", 283 | "steps": [ 284 | { 285 | "color": "blue", 286 | "value": null 287 | } 288 | ] 289 | } 290 | }, 291 | "overrides": [] 292 | }, 293 | "gridPos": { 294 | "h": 5, 295 | "w": 4, 296 | "x": 20, 297 | "y": 6 298 | }, 299 | "id": 3, 300 | "options": { 301 | "colorMode": "value", 302 | "graphMode": "none", 303 | "justifyMode": "auto", 304 | "orientation": "auto", 305 | "reduceOptions": { 306 | "calcs": ["lastNotNull"], 307 | "fields": "", 308 | "values": false 309 | }, 310 | "textMode": "auto" 311 | }, 312 | "pluginVersion": "10.0.2", 313 | "targets": [ 314 | { 315 | "datasource": { 316 | "type": "prometheus", 317 | "uid": "${DS_PROMETHEUS}" 318 | }, 319 | "editorMode": "code", 320 | "exemplar": true, 321 | "expr": "count(count by (node) (kube_node_info))", 322 | "instant": false, 323 | "range": true, 324 | "refId": "A" 325 | } 326 | ], 327 | "title": "# of Nodes", 328 | "type": "stat" 329 | }, 330 | { 331 | "datasource": { 332 | "type": "prometheus", 333 | "uid": "${DS_PROMETHEUS}" 334 | }, 335 | "description": "", 336 | "fieldConfig": { 337 | "defaults": { 338 | "color": { 339 | "mode": "continuous-GrYlRd" 340 | }, 341 | "decimals": 2, 342 | "mappings": [], 343 | "max": 1, 344 | "min": 0, 345 | "thresholds": { 346 | "mode": "percentage", 347 | "steps": [ 348 | { 349 | "color": "green", 350 | "value": null 351 | }, 352 | { 353 | "color": "red", 354 | "value": 80 355 | } 356 | ] 357 | }, 358 | "unit": "percentunit" 359 | }, 360 | "overrides": [] 361 | }, 362 | "gridPos": { 363 | "h": 8, 364 | "w": 11, 365 | "x": 0, 366 | "y": 9 367 | }, 368 | "id": 2, 369 | "options": { 370 | "displayMode": "lcd", 371 | "minVizHeight": 10, 372 | "minVizWidth": 0, 373 | "orientation": "horizontal", 374 | "reduceOptions": { 375 | "calcs": ["lastNotNull"], 376 | "fields": "", 377 | "values": false 378 | }, 379 | "showUnfilled": true, 380 | "valueMode": "color" 381 | }, 382 | "pluginVersion": "10.0.2", 383 | "targets": [ 384 | { 385 | "datasource": { 386 | "type": "prometheus", 387 | "uid": "${DS_PROMETHEUS}" 388 | }, 389 | "editorMode": "code", 390 | "exemplar": true, 391 | "expr": "sum(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / sum(node_memory_MemTotal_bytes)", 392 | "instant": false, 393 | "interval": "", 394 | "legendFormat": "Real", 395 | "range": true, 396 | "refId": "A" 397 | }, 398 | { 399 | "datasource": { 400 | "type": "prometheus", 401 | "uid": "${DS_PROMETHEUS}" 402 | }, 403 | "editorMode": "code", 404 | "expr": "sum(kube_pod_container_resource_requests{resource=\"memory\"}) / sum(machine_memory_bytes)", 405 | "hide": false, 406 | "instant": false, 407 | "legendFormat": "Requests", 408 | "range": true, 409 | "refId": "B" 410 | }, 411 | { 412 | "datasource": { 413 | "type": "prometheus", 414 | "uid": "${DS_PROMETHEUS}" 415 | }, 416 | "editorMode": "code", 417 | "expr": "sum(kube_pod_container_resource_limits{resource=\"memory\"}) / sum(machine_memory_bytes)", 418 | "hide": false, 419 | "instant": false, 420 | "legendFormat": "Limits", 421 | "range": true, 422 | "refId": "C" 423 | } 424 | ], 425 | "title": "Global RAM Usage", 426 | "type": "bargauge" 427 | }, 428 | { 429 | "datasource": { 430 | "type": "prometheus", 431 | "uid": "${DS_PROMETHEUS}" 432 | }, 433 | "fieldConfig": { 434 | "defaults": { 435 | "color": { 436 | "mode": "palette-classic" 437 | }, 438 | "custom": { 439 | "axisCenteredZero": false, 440 | "axisColorMode": "text", 441 | "axisLabel": "", 442 | "axisPlacement": "auto", 443 | "barAlignment": 0, 444 | "drawStyle": "line", 445 | "fillOpacity": 10, 446 | "gradientMode": "opacity", 447 | "hideFrom": { 448 | "legend": false, 449 | "tooltip": false, 450 | "viz": false 451 | }, 452 | "lineInterpolation": "linear", 453 | "lineStyle": { 454 | "fill": "solid" 455 | }, 456 | "lineWidth": 1, 457 | "pointSize": 5, 458 | "scaleDistribution": { 459 | "type": "linear" 460 | }, 461 | "showPoints": "never", 462 | "spanNulls": false, 463 | "stacking": { 464 | "group": "A", 465 | "mode": "none" 466 | }, 467 | "thresholdsStyle": { 468 | "mode": "off" 469 | } 470 | }, 471 | "mappings": [], 472 | "thresholds": { 473 | "mode": "absolute", 474 | "steps": [ 475 | { 476 | "color": "green", 477 | "value": null 478 | }, 479 | { 480 | "color": "red", 481 | "value": 80 482 | } 483 | ] 484 | }, 485 | "unit": "percentunit" 486 | }, 487 | "overrides": [] 488 | }, 489 | "gridPos": { 490 | "h": 8, 491 | "w": 9, 492 | "x": 11, 493 | "y": 9 494 | }, 495 | "id": 14, 496 | "options": { 497 | "legend": { 498 | "calcs": [], 499 | "displayMode": "list", 500 | "placement": "bottom", 501 | "showLegend": false 502 | }, 503 | "tooltip": { 504 | "mode": "single", 505 | "sort": "none" 506 | } 507 | }, 508 | "targets": [ 509 | { 510 | "datasource": { 511 | "type": "prometheus", 512 | "uid": "${DS_PROMETHEUS}" 513 | }, 514 | "editorMode": "code", 515 | "expr": "(\n instance:node_load1_per_cpu:ratio{job=\"node-exporter\", cluster=\"\"}\n / scalar(count(instance:node_load1_per_cpu:ratio{job=\"node-exporter\", cluster=\"\"}))\n) != 0\n", 516 | "instant": false, 517 | "range": true, 518 | "refId": "A" 519 | } 520 | ], 521 | "title": "CPU Saturation (Load1 / CPU)", 522 | "type": "timeseries" 523 | }, 524 | { 525 | "datasource": { 526 | "type": "prometheus", 527 | "uid": "${DS_PROMETHEUS}" 528 | }, 529 | "fieldConfig": { 530 | "defaults": { 531 | "color": { 532 | "mode": "thresholds" 533 | }, 534 | "mappings": [], 535 | "thresholds": { 536 | "mode": "absolute", 537 | "steps": [ 538 | { 539 | "color": "blue", 540 | "value": null 541 | } 542 | ] 543 | } 544 | }, 545 | "overrides": [] 546 | }, 547 | "gridPos": { 548 | "h": 5, 549 | "w": 4, 550 | "x": 20, 551 | "y": 11 552 | }, 553 | "id": 20, 554 | "options": { 555 | "colorMode": "value", 556 | "graphMode": "none", 557 | "justifyMode": "auto", 558 | "orientation": "auto", 559 | "reduceOptions": { 560 | "calcs": ["lastNotNull"], 561 | "fields": "", 562 | "values": false 563 | }, 564 | "textMode": "auto" 565 | }, 566 | "pluginVersion": "10.0.2", 567 | "targets": [ 568 | { 569 | "datasource": { 570 | "type": "prometheus", 571 | "uid": "${DS_PROMETHEUS}" 572 | }, 573 | "editorMode": "code", 574 | "exemplar": false, 575 | "expr": "sum(kube_deployment_labels)", 576 | "instant": false, 577 | "legendFormat": "Deployments", 578 | "range": true, 579 | "refId": "A" 580 | } 581 | ], 582 | "title": "# of Deployments", 583 | "type": "stat" 584 | }, 585 | { 586 | "datasource": { 587 | "type": "prometheus", 588 | "uid": "${DS_PROMETHEUS}" 589 | }, 590 | "description": "", 591 | "fieldConfig": { 592 | "defaults": { 593 | "color": { 594 | "mode": "thresholds" 595 | }, 596 | "mappings": [], 597 | "thresholds": { 598 | "mode": "absolute", 599 | "steps": [ 600 | { 601 | "color": "blue", 602 | "value": null 603 | } 604 | ] 605 | } 606 | }, 607 | "overrides": [] 608 | }, 609 | "gridPos": { 610 | "h": 5, 611 | "w": 4, 612 | "x": 20, 613 | "y": 16 614 | }, 615 | "id": 19, 616 | "options": { 617 | "colorMode": "value", 618 | "graphMode": "none", 619 | "justifyMode": "auto", 620 | "orientation": "auto", 621 | "reduceOptions": { 622 | "calcs": ["lastNotNull"], 623 | "fields": "", 624 | "values": false 625 | }, 626 | "textMode": "auto" 627 | }, 628 | "pluginVersion": "10.0.2", 629 | "targets": [ 630 | { 631 | "datasource": { 632 | "type": "prometheus", 633 | "uid": "${DS_PROMETHEUS}" 634 | }, 635 | "editorMode": "code", 636 | "expr": "count(kube_namespace_created)", 637 | "instant": false, 638 | "range": true, 639 | "refId": "A" 640 | } 641 | ], 642 | "title": "# of Namespaces", 643 | "type": "stat" 644 | }, 645 | { 646 | "collapsed": true, 647 | "gridPos": { 648 | "h": 1, 649 | "w": 24, 650 | "x": 0, 651 | "y": 21 652 | }, 653 | "id": 13, 654 | "panels": [ 655 | { 656 | "datasource": { 657 | "type": "prometheus", 658 | "uid": "${DS_PROMETHEUS}" 659 | }, 660 | "description": "", 661 | "fieldConfig": { 662 | "defaults": { 663 | "color": { 664 | "mode": "continuous-GrYlRd" 665 | }, 666 | "custom": { 667 | "axisCenteredZero": false, 668 | "axisColorMode": "text", 669 | "axisLabel": "CPU %", 670 | "axisPlacement": "auto", 671 | "barAlignment": 0, 672 | "drawStyle": "line", 673 | "fillOpacity": 10, 674 | "gradientMode": "scheme", 675 | "hideFrom": { 676 | "legend": false, 677 | "tooltip": false, 678 | "viz": false 679 | }, 680 | "lineInterpolation": "smooth", 681 | "lineWidth": 2, 682 | "pointSize": 5, 683 | "scaleDistribution": { 684 | "type": "linear" 685 | }, 686 | "showPoints": "never", 687 | "spanNulls": false, 688 | "stacking": { 689 | "group": "A", 690 | "mode": "none" 691 | }, 692 | "thresholdsStyle": { 693 | "mode": "off" 694 | } 695 | }, 696 | "decimals": 0, 697 | "mappings": [], 698 | "thresholds": { 699 | "mode": "absolute", 700 | "steps": [ 701 | { 702 | "color": "green" 703 | }, 704 | { 705 | "color": "#EAB839", 706 | "value": 0.5 707 | }, 708 | { 709 | "color": "red", 710 | "value": 0.7 711 | } 712 | ] 713 | }, 714 | "unit": "percentunit" 715 | }, 716 | "overrides": [] 717 | }, 718 | "gridPos": { 719 | "h": 8, 720 | "w": 9, 721 | "x": 0, 722 | "y": 2 723 | }, 724 | "id": 5, 725 | "options": { 726 | "legend": { 727 | "calcs": [], 728 | "displayMode": "list", 729 | "placement": "bottom", 730 | "showLegend": false 731 | }, 732 | "tooltip": { 733 | "mode": "single", 734 | "sort": "none" 735 | } 736 | }, 737 | "targets": [ 738 | { 739 | "datasource": { 740 | "type": "prometheus", 741 | "uid": "${DS_PROMETHEUS}" 742 | }, 743 | "editorMode": "code", 744 | "exemplar": true, 745 | "expr": "avg(1-rate(node_cpu_seconds_total{mode=\"idle\"}[$__rate_interval]))", 746 | "instant": false, 747 | "interval": "", 748 | "legendFormat": "CPU Usage in %", 749 | "range": true, 750 | "refId": "A" 751 | } 752 | ], 753 | "title": "Cluster CPU Utilization", 754 | "type": "timeseries" 755 | } 756 | ], 757 | "title": "CPU", 758 | "type": "row" 759 | }, 760 | { 761 | "collapsed": true, 762 | "gridPos": { 763 | "h": 1, 764 | "w": 24, 765 | "x": 0, 766 | "y": 22 767 | }, 768 | "id": 16, 769 | "panels": [], 770 | "title": "Memory", 771 | "type": "row" 772 | }, 773 | { 774 | "collapsed": true, 775 | "gridPos": { 776 | "h": 1, 777 | "w": 24, 778 | "x": 0, 779 | "y": 23 780 | }, 781 | "id": 18, 782 | "panels": [], 783 | "title": "Nodes", 784 | "type": "row" 785 | }, 786 | { 787 | "collapsed": true, 788 | "gridPos": { 789 | "h": 1, 790 | "w": 24, 791 | "x": 0, 792 | "y": 24 793 | }, 794 | "id": 17, 795 | "panels": [ 796 | { 797 | "datasource": { 798 | "type": "prometheus", 799 | "uid": "${DS_PROMETHEUS}" 800 | }, 801 | "fieldConfig": { 802 | "defaults": { 803 | "color": { 804 | "mode": "palette-classic" 805 | }, 806 | "custom": { 807 | "axisCenteredZero": false, 808 | "axisColorMode": "text", 809 | "axisLabel": "", 810 | "axisPlacement": "auto", 811 | "barAlignment": 0, 812 | "drawStyle": "line", 813 | "fillOpacity": 25, 814 | "gradientMode": "opacity", 815 | "hideFrom": { 816 | "legend": false, 817 | "tooltip": false, 818 | "viz": false 819 | }, 820 | "lineInterpolation": "linear", 821 | "lineWidth": 2, 822 | "pointSize": 5, 823 | "scaleDistribution": { 824 | "type": "linear" 825 | }, 826 | "showPoints": "never", 827 | "spanNulls": false, 828 | "stacking": { 829 | "group": "A", 830 | "mode": "none" 831 | }, 832 | "thresholdsStyle": { 833 | "mode": "off" 834 | } 835 | }, 836 | "mappings": [], 837 | "thresholds": { 838 | "mode": "absolute", 839 | "steps": [ 840 | { 841 | "color": "green" 842 | }, 843 | { 844 | "color": "red", 845 | "value": 80 846 | } 847 | ] 848 | }, 849 | "unit": "short" 850 | }, 851 | "overrides": [] 852 | }, 853 | "gridPos": { 854 | "h": 13, 855 | "w": 18, 856 | "x": 0, 857 | "y": 25 858 | }, 859 | "id": 7, 860 | "options": { 861 | "legend": { 862 | "calcs": ["min", "max", "mean"], 863 | "displayMode": "table", 864 | "placement": "right", 865 | "showLegend": true 866 | }, 867 | "tooltip": { 868 | "mode": "multi", 869 | "sort": "none" 870 | } 871 | }, 872 | "targets": [ 873 | { 874 | "datasource": { 875 | "type": "prometheus", 876 | "uid": "${DS_PROMETHEUS}" 877 | }, 878 | "editorMode": "code", 879 | "exemplar": true, 880 | "expr": "sum(kube_pod_status_qos_class) by (qos_class)", 881 | "instant": false, 882 | "legendFormat": "{{ qos_class }} pods", 883 | "range": true, 884 | "refId": "A" 885 | }, 886 | { 887 | "datasource": { 888 | "type": "prometheus", 889 | "uid": "${DS_PROMETHEUS}" 890 | }, 891 | "editorMode": "code", 892 | "expr": "sum(kube_pod_info)", 893 | "hide": false, 894 | "instant": false, 895 | "interval": "", 896 | "legendFormat": "Total pods", 897 | "range": true, 898 | "refId": "B" 899 | } 900 | ], 901 | "title": "Kubernetes Pods QoS classes", 902 | "type": "timeseries" 903 | } 904 | ], 905 | "title": "Pods", 906 | "type": "row" 907 | }, 908 | { 909 | "collapsed": true, 910 | "gridPos": { 911 | "h": 1, 912 | "w": 24, 913 | "x": 0, 914 | "y": 25 915 | }, 916 | "id": 12, 917 | "panels": [ 918 | { 919 | "datasource": { 920 | "type": "prometheus", 921 | "uid": "${DS_PROMETHEUS}" 922 | }, 923 | "description": "Dropped noisy virtual devices for readability.", 924 | "fieldConfig": { 925 | "defaults": { 926 | "color": { 927 | "mode": "palette-classic" 928 | }, 929 | "custom": { 930 | "axisCenteredZero": false, 931 | "axisColorMode": "text", 932 | "axisLabel": "Bandwidth", 933 | "axisPlacement": "auto", 934 | "barAlignment": 0, 935 | "drawStyle": "line", 936 | "fillOpacity": 25, 937 | "gradientMode": "opacity", 938 | "hideFrom": { 939 | "legend": false, 940 | "tooltip": false, 941 | "viz": false 942 | }, 943 | "lineInterpolation": "linear", 944 | "lineWidth": 2, 945 | "pointSize": 5, 946 | "scaleDistribution": { 947 | "type": "linear" 948 | }, 949 | "showPoints": "never", 950 | "spanNulls": false, 951 | "stacking": { 952 | "group": "A", 953 | "mode": "none" 954 | }, 955 | "thresholdsStyle": { 956 | "mode": "off" 957 | } 958 | }, 959 | "mappings": [], 960 | "thresholds": { 961 | "mode": "absolute", 962 | "steps": [ 963 | { 964 | "color": "green" 965 | }, 966 | { 967 | "color": "red", 968 | "value": 80 969 | } 970 | ] 971 | }, 972 | "unit": "bytes" 973 | }, 974 | "overrides": [ 975 | { 976 | "__systemRef": "hideSeriesFrom", 977 | "matcher": { 978 | "id": "byNames", 979 | "options": { 980 | "mode": "exclude", 981 | "names": ["Recieved: bridge"], 982 | "prefix": "All except:", 983 | "readOnly": true 984 | } 985 | }, 986 | "properties": [ 987 | { 988 | "id": "custom.hideFrom", 989 | "value": { 990 | "legend": false, 991 | "tooltip": false, 992 | "viz": true 993 | } 994 | } 995 | ] 996 | } 997 | ] 998 | }, 999 | "gridPos": { 1000 | "h": 8, 1001 | "w": 12, 1002 | "x": 0, 1003 | "y": 26 1004 | }, 1005 | "id": 15, 1006 | "options": { 1007 | "legend": { 1008 | "calcs": [], 1009 | "displayMode": "list", 1010 | "placement": "bottom", 1011 | "showLegend": true 1012 | }, 1013 | "tooltip": { 1014 | "mode": "single", 1015 | "sort": "none" 1016 | } 1017 | }, 1018 | "targets": [ 1019 | { 1020 | "datasource": { 1021 | "type": "prometheus", 1022 | "uid": "${DS_PROMETHEUS}" 1023 | }, 1024 | "editorMode": "code", 1025 | "exemplar": true, 1026 | "expr": "sum(rate(node_network_receive_bytes_total{device!~\"lxc.*|veth.*\"}[$__rate_interval])) by (device)", 1027 | "instant": false, 1028 | "legendFormat": "Recieved: {{device}}", 1029 | "range": true, 1030 | "refId": "A" 1031 | }, 1032 | { 1033 | "datasource": { 1034 | "type": "prometheus", 1035 | "uid": "${DS_PROMETHEUS}" 1036 | }, 1037 | "editorMode": "code", 1038 | "exemplar": true, 1039 | "expr": "- sum(rate(node_network_transmit_bytes_total{device!~\"lxc.*|veth.*\"}[$__rate_interval])) by (device)", 1040 | "hide": false, 1041 | "instant": false, 1042 | "interval": "", 1043 | "legendFormat": "Transmitted: {{device}}", 1044 | "range": true, 1045 | "refId": "B" 1046 | } 1047 | ], 1048 | "title": "Global Network Utilization by Device", 1049 | "type": "timeseries" 1050 | } 1051 | ], 1052 | "title": "Network", 1053 | "type": "row" 1054 | }, 1055 | { 1056 | "collapsed": true, 1057 | "gridPos": { 1058 | "h": 1, 1059 | "w": 24, 1060 | "x": 0, 1061 | "y": 26 1062 | }, 1063 | "id": 11, 1064 | "panels": [ 1065 | { 1066 | "datasource": { 1067 | "type": "prometheus", 1068 | "uid": "${DS_PROMETHEUS}" 1069 | }, 1070 | "description": "", 1071 | "fieldConfig": { 1072 | "defaults": { 1073 | "color": { 1074 | "mode": "palette-classic" 1075 | }, 1076 | "custom": { 1077 | "axisCenteredZero": false, 1078 | "axisColorMode": "text", 1079 | "axisLabel": "", 1080 | "axisPlacement": "auto", 1081 | "barAlignment": 0, 1082 | "drawStyle": "line", 1083 | "fillOpacity": 10, 1084 | "gradientMode": "opacity", 1085 | "hideFrom": { 1086 | "legend": false, 1087 | "tooltip": false, 1088 | "viz": false 1089 | }, 1090 | "lineInterpolation": "linear", 1091 | "lineStyle": { 1092 | "fill": "solid" 1093 | }, 1094 | "lineWidth": 1, 1095 | "pointSize": 5, 1096 | "scaleDistribution": { 1097 | "type": "linear" 1098 | }, 1099 | "showPoints": "never", 1100 | "spanNulls": false, 1101 | "stacking": { 1102 | "group": "A", 1103 | "mode": "none" 1104 | }, 1105 | "thresholdsStyle": { 1106 | "mode": "off" 1107 | } 1108 | }, 1109 | "mappings": [], 1110 | "min": 0, 1111 | "thresholds": { 1112 | "mode": "absolute", 1113 | "steps": [ 1114 | { 1115 | "color": "green" 1116 | }, 1117 | { 1118 | "color": "red", 1119 | "value": 80 1120 | } 1121 | ] 1122 | } 1123 | }, 1124 | "overrides": [] 1125 | }, 1126 | "gridPos": { 1127 | "h": 8, 1128 | "w": 12, 1129 | "x": 0, 1130 | "y": 27 1131 | }, 1132 | "id": 10, 1133 | "options": { 1134 | "legend": { 1135 | "calcs": [], 1136 | "displayMode": "list", 1137 | "placement": "bottom", 1138 | "showLegend": true 1139 | }, 1140 | "tooltip": { 1141 | "mode": "single", 1142 | "sort": "none" 1143 | } 1144 | }, 1145 | "targets": [ 1146 | { 1147 | "datasource": { 1148 | "type": "prometheus", 1149 | "uid": "${DS_PROMETHEUS}" 1150 | }, 1151 | "editorMode": "code", 1152 | "expr": "rate(node_disk_read_bytes_total{job=\"node-exporter\", instance=\"192.168.49.2:9100\", device=~\"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|md.+|dasd.+)\"}[$__rate_interval])", 1153 | "instant": false, 1154 | "legendFormat": "vda read", 1155 | "range": true, 1156 | "refId": "A" 1157 | }, 1158 | { 1159 | "datasource": { 1160 | "type": "prometheus", 1161 | "uid": "${DS_PROMETHEUS}" 1162 | }, 1163 | "editorMode": "code", 1164 | "expr": "rate(node_disk_written_bytes_total{job=\"node-exporter\", instance=\"192.168.49.2:9100\", device=~\"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|md.+|dasd.+)\"}[$__rate_interval])", 1165 | "hide": false, 1166 | "instant": false, 1167 | "legendFormat": "vda io time", 1168 | "range": true, 1169 | "refId": "B" 1170 | }, 1171 | { 1172 | "datasource": { 1173 | "type": "prometheus", 1174 | "uid": "${DS_PROMETHEUS}" 1175 | }, 1176 | "editorMode": "code", 1177 | "expr": "rate(node_disk_io_time_seconds_total{job=\"node-exporter\", instance=\"192.168.49.2:9100\", device=~\"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|md.+|dasd.+)\"}[$__rate_interval])", 1178 | "hide": false, 1179 | "instant": false, 1180 | "legendFormat": "vda written", 1181 | "range": true, 1182 | "refId": "C" 1183 | } 1184 | ], 1185 | "title": "Disk I/O", 1186 | "type": "timeseries" 1187 | } 1188 | ], 1189 | "title": "Disk", 1190 | "type": "row" 1191 | } 1192 | ] 1193 | --------------------------------------------------------------------------------