├── .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 |
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 |
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 |
37 | Logout
38 |
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 | 
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 | 
73 | 
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 |
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 |
110 |
Before logging in:
111 |
112 | Please install Helm
113 | Free up Port 3000
114 |
115 |
116 | Learn More
117 |
118 |
119 |
*/}
120 |
121 |
122 |
128 |
Get started
129 |
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 |
122 |
Manage & monitor your K8 cluster metrics with ease
123 |
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 |
--------------------------------------------------------------------------------