├── assets ├── demo-gifs │ ├── login.gif │ ├── visualizer.gif │ ├── cost-analysis.gif │ └── local-cluster-metrics.gif └── logo-no-background.png ├── .prettierrc.json ├── src ├── client │ ├── components │ │ ├── auth │ │ │ ├── authContext.jsx │ │ │ ├── useAuth.jsx │ │ │ ├── PrivateRoute.jsx │ │ │ └── AuthProvider.jsx │ │ ├── MetricPanel.jsx │ │ ├── dashboards │ │ │ ├── ClusterVisualizerDashboard.jsx │ │ │ ├── CloudMetricsDashboard.jsx │ │ │ ├── ClusterMetricsDashboard.jsx │ │ │ └── CostAnalysisDashboard.jsx │ │ ├── AnimatedLogo.jsx │ │ ├── NavBar.jsx │ │ ├── CostTable.jsx │ │ └── SideBar.jsx │ ├── main.jsx │ ├── pages │ │ ├── App.jsx │ │ ├── Home.jsx │ │ ├── Signup.jsx │ │ └── SignIn.jsx │ └── styles │ │ └── App.css ├── server │ ├── controllers │ │ └── authController.js │ ├── routes │ │ └── authRoutes.js │ ├── models │ │ └── mongoooseModel.js │ ├── server.js │ └── config │ │ └── passport.js └── constants.js ├── .env ├── .gitignore ├── vite.config.js ├── index.html ├── .eslintrc.js ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── README.md └── SETUP.md /assets/demo-gifs/login.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/spyglass/HEAD/assets/demo-gifs/login.gif -------------------------------------------------------------------------------- /assets/demo-gifs/visualizer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/spyglass/HEAD/assets/demo-gifs/visualizer.gif -------------------------------------------------------------------------------- /assets/logo-no-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/spyglass/HEAD/assets/logo-no-background.png -------------------------------------------------------------------------------- /assets/demo-gifs/cost-analysis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/spyglass/HEAD/assets/demo-gifs/cost-analysis.gif -------------------------------------------------------------------------------- /assets/demo-gifs/local-cluster-metrics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/spyglass/HEAD/assets/demo-gifs/local-cluster-metrics.gif -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "bracketSpacing": true, 8 | "arrowParens": "always" 9 | } -------------------------------------------------------------------------------- /src/client/components/auth/authContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | //Child components of AuthProvider will read the current context value (ie, auth) from AuthProvider 4 | export const authContext = createContext(); 5 | -------------------------------------------------------------------------------- /src/client/components/MetricPanel.jsx: -------------------------------------------------------------------------------- 1 | function MetricPanel({ url }) { 2 | return ( 3 | // display metrics retrieved from Grafana in an iframe 4 |
5 | 6 |
7 | ); 8 | } 9 | 10 | export default MetricPanel; 11 | -------------------------------------------------------------------------------- /src/client/components/auth/useAuth.jsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { authContext } from './authContext'; 3 | //useAuth custom hook for child compenents to get the auth obj 4 | export function useAuth() { 5 | //useContext hook returns value (ie, auth) from provider component 6 | return useContext(authContext); 7 | } 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Example ENV file (please replace in your own .env) 2 | 3 | MONGO_URI="mongodb+srv://dummyAcc:HVcsVJuNv2fTCJJl@spyglassdev.jmhr4fn.mongodb.net/?retryWrites=true&w=majority" 4 | VITE_LOCALCLUSTERIP=localhost:8000 5 | VITE_LOCALCLUSTERNAME=IMOt5Yf4z 6 | VITE_CLOUDCLUSTERIP=a5f23f08f01e34e6c883489e8cfef487-101927145.us-east-1.elb.amazonaws.com 7 | VITE_CLOUDCLUSTERNAME=bgPQC9f4z -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 8080, 9 | proxy: { 10 | '/auth/**': { 11 | target: 'http://localhost:3333', 12 | secure: false 13 | } 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/client/components/dashboards/ClusterVisualizerDashboard.jsx: -------------------------------------------------------------------------------- 1 | function ClusterVisualizerDashboard() { 2 | const visualURL = 'http://localhost:9000/'; 3 | return ( 4 | // display graph nodes from Kubeview 5 |
6 | 7 |
8 | ); 9 | } 10 | export default ClusterVisualizerDashboard; 11 | -------------------------------------------------------------------------------- /src/client/components/auth/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import { useAuth } from './useAuth'; 2 | import { Navigate } from 'react-router-dom'; 3 | //PrivateRoute is a wrapper for routes 4 | //redirects to signIn if user is not authenticated 5 | function PrivateRoute({ children }) { 6 | const auth = useAuth(); 7 | if (!auth.user) { 8 | return ; 9 | } 10 | return children; 11 | } 12 | 13 | export default PrivateRoute; 14 | -------------------------------------------------------------------------------- /src/server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import { User } from '../models/mongoooseModel.js'; 2 | 3 | export const authController = {}; 4 | 5 | authController.credSuccess = async (req, res, next) => { 6 | const username = req.body.username; 7 | try { 8 | const userData = await User.findOne({ 'local.username': username }); 9 | res.locals.userData = userData; 10 | next(); 11 | } catch (err) { 12 | next(err); 13 | } 14 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | Spyglass 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | root: true, 4 | env: { 5 | browser: true, 6 | node: true, 7 | jest: true 8 | }, 9 | rules: { 10 | 'arrow-parens': 'off', 11 | 'consistent-return': 'off', 12 | 'func-names': 'off', 13 | 'no-console': 'off', 14 | radix: 'off', 15 | 'react/button-has-type': 'off', 16 | 'react/destructuring-assignment': 'off', 17 | 'react/jsx-filename-extension': 'off', 18 | 'react/prop-types': 'off' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/client/components/AnimatedLogo.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import spyglass from '../../../assets/logo-no-background.png'; 3 | import { motion } from 'framer-motion'; 4 | 5 | function AnimatedLogo() { 6 | return ( 7 | // wrap motion.div around spyglass logo to add rotating animations 8 | 13 | spyglass-logo 14 | 15 | ); 16 | } 17 | export default AnimatedLogo; 18 | -------------------------------------------------------------------------------- /src/client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AppBar, Toolbar, Button } from '@mui/material'; 3 | import { useAuth } from './auth/useAuth'; 4 | 5 | function NavBar() { 6 | const auth = useAuth(); 7 | 8 | return ( 9 | // display navigation bar with "sign out" button that has access to auth 10 | 16 | 17 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default NavBar; 26 | -------------------------------------------------------------------------------- /src/client/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './pages/App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import { ThemeProvider, createTheme } from '@mui/material'; 6 | 7 | // create custom theme from mood icons 8 | const theme = createTheme({ 9 | palette: { 10 | White: { 11 | main: '#fff' 12 | }, 13 | Black: { 14 | main: '#1a1a1a' 15 | } 16 | } 17 | }); 18 | 19 | ReactDOM.createRoot(document.getElementById('root')).render( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /src/client/components/dashboards/CloudMetricsDashboard.jsx: -------------------------------------------------------------------------------- 1 | import MetricPanel from '../MetricPanel'; 2 | import { cloudDashboardURLs } from '../../../constants'; 3 | import Grid from '@mui/material/Grid'; 4 | 5 | function CloudMetricsDashboard() { 6 | const panels = cloudDashboardURLs.map((url, idx) => ( 7 | 8 | 9 | 10 | )); 11 | return ( 12 | // display panels for a cloud cluster in a grid container 13 | 22 | {panels} 23 | 24 | ); 25 | } 26 | 27 | export default CloudMetricsDashboard; 28 | -------------------------------------------------------------------------------- /src/client/components/dashboards/ClusterMetricsDashboard.jsx: -------------------------------------------------------------------------------- 1 | import MetricPanel from '../MetricPanel'; 2 | import { localDashboardURLs } from '../../../constants'; 3 | import Grid from '@mui/material/Grid'; 4 | 5 | function ClusterMetricsDashboard() { 6 | const panels = localDashboardURLs.map((url, idx) => ( 7 | 8 | 9 | 10 | )); 11 | return ( 12 | // display panels for a local cluster in a grid container 13 | 22 | {panels} 23 | 24 | ); 25 | } 26 | export default ClusterMetricsDashboard; 27 | -------------------------------------------------------------------------------- /src/client/pages/App.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import Home from './Home'; 3 | import SignIn from './SignIn'; 4 | import SignUp from './SignUp'; 5 | import { AuthProvider } from '../components/auth/AuthProvider'; 6 | import PrivateRoute from '../components/auth/PrivateRoute'; 7 | import '../styles/App.css'; 8 | 9 | function App() { 10 | return ( 11 | // home page is protected route and requires authorization 12 | 13 | 14 | } /> 15 | } /> 16 | {/* home page contains routes to different dashboards */} 17 | 21 | 22 | 23 | } 24 | > 25 | 26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import NavBar from '../components/NavBar'; 2 | import SideBar from '../components/SideBar'; 3 | import Box from '@mui/material/Box'; 4 | import ClusterMetricsDashboard from '../components/dashboards/ClusterMetricsDashboard'; 5 | import CostAnalysisDashboard from '../components/dashboards/CostAnalysisDashboard'; 6 | import CloudMetricsDashboard from '../components/dashboards/CloudMetricsDashboard'; 7 | import ClusterVisualizerDashboard from '../components/dashboards/ClusterVisualizerDashboard'; 8 | import { Routes, Route } from 'react-router-dom'; 9 | import '../styles/App.css'; 10 | 11 | function Home() { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | 24 |
25 | ); 26 | } 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Spyglass 2 | Thank you for your contribution! Contributions are welcome and are greatly appreciated. 3 | 4 | ## Reporting Bugs 5 | All code changes happen through Github Pull Requests and we actively welcome them. To submit your pull request, follow the steps below: 6 | 7 | ## Pull Requests 8 | 1. Fork the repo and create your feature branch. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. Issue that pull request! 11 | 4. Specify what you changed in details when you are doing pull request. 12 | 13 | Note: Any contributions you make will be under the MIT Software License and your submissions are understood to be under the same that covers the project. Please reach out to the team if you have any questions. 14 | 15 | ## Issues 16 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 17 | 18 | ## License 19 | By contributing, you agree that your contributions will be licensed under Spyglass' MIT License. 20 | 21 | ## References 22 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md) 23 | -------------------------------------------------------------------------------- /src/server/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | //import express 2 | import express from 'express'; 3 | import passport from 'passport'; 4 | export const authRouter = express.Router(); 5 | 6 | import { authController } from '../controllers/authController.js'; 7 | // Process login form 8 | authRouter.post( 9 | '/login', 10 | passport.authenticate('local-login', { 11 | successMessage: 'authenticated', 12 | failureRedirect: '/auth/loginfailure' 13 | }), 14 | // authController.credSuccess, 15 | (req, res) => { 16 | res.status(203).json(req.session); 17 | } 18 | ); 19 | 20 | // Process signup form 21 | authRouter.post( 22 | '/signup', 23 | passport.authenticate('local-signup', { 24 | successMessage: 'authenticated', 25 | failureRedirect: '/auth/signupfailure' 26 | }), 27 | // authController.credSuccess, 28 | (req, res) => { 29 | res.status(203).json(req.session); 30 | } 31 | ); 32 | 33 | // get user info and return via res.status(203).json(res.loclas.userInfo) 34 | authRouter.get('/credentials', authController.credSuccess, (req, res) => { 35 | res.status(203).json(res.locals.userData); 36 | }); 37 | 38 | // on failed login 39 | authRouter.get('/loginfailure', (req, res) => { 40 | res.status(401).json('incorrect credentials'); 41 | }); 42 | 43 | // on failed signup 44 | authRouter.get('/signupfailure', (req, res) => { 45 | res.status(401).json('incorrect format'); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "concurrently \"vite --host\" \"nodemon src/server/server.js\"", 8 | "start": "nodemon src/server/server.js", 9 | "build": "vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.10.6", 14 | "@emotion/styled": "^11.10.6", 15 | "@kubernetes/client-node": "^0.18.1", 16 | "@mui/icons-material": "^5.11.11", 17 | "@mui/material": "^5.11.13", 18 | "axios": "^1.3.4", 19 | "bcrypt": "^5.1.0", 20 | "concurrently": "^7.6.0", 21 | "connect-mongo": "^5.0.0", 22 | "cors": "^2.8.5", 23 | "crypto": "^1.0.1", 24 | "dotenv": "^16.0.3", 25 | "express": "^4.18.2", 26 | "express-session": "^1.17.3", 27 | "framer-motion": "^10.3.2", 28 | "mongoose": "^7.0.2", 29 | "nodemon": "^2.0.21", 30 | "passport": "^0.6.0", 31 | "passport-local": "^1.0.0", 32 | "path": "^0.12.7", 33 | "prom-client": "^14.2.0", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-router-dom": "^6.9.0" 37 | }, 38 | "devDependencies": { 39 | "@types/react": "^18.0.27", 40 | "@types/react-dom": "^18.0.10", 41 | "@vitejs/plugin-react": "^3.1.0", 42 | "eslint-plugin-react-hooks": "^4.6.0", 43 | "vite": "^4.1.0", 44 | "vite-plugin-terminal": "^1.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/client/styles/App.css: -------------------------------------------------------------------------------- 1 | /* styling for general app*/ 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | html { 7 | background: #1a1a1af4; 8 | } 9 | 10 | .main { 11 | margin-left: 18rem; 12 | margin-top: 3rem; 13 | } 14 | 15 | /* styling for navBar*/ 16 | .NavBar { 17 | align-items: end; 18 | padding: 0.5rem; 19 | } 20 | .NavBar button:hover { 21 | color: #0074d9; 22 | } 23 | 24 | /* styling for links in sideBar */ 25 | .sideBar a { 26 | text-decoration: none; 27 | color: #fff; 28 | } 29 | .sideBar a:hover { 30 | color: #0074d9; 31 | } 32 | .sideBar a span { 33 | font-size: 1.6rem; 34 | padding: 0.5rem 0; 35 | } 36 | 37 | /* icon in sideBar */ 38 | .MuiSvgIcon-root { 39 | color: #0074d9; 40 | } 41 | 42 | /* spacing between icon and text in sideBar */ 43 | .MuiButtonBase-root div { 44 | min-width: 35px; 45 | } 46 | 47 | /* styling for spgylass logo in sideBar */ 48 | .spyglass-logo { 49 | height: auto; 50 | max-width: 100%; 51 | margin-top: 4rem; 52 | margin-bottom: 2rem; 53 | } 54 | 55 | /* styling for cost table container */ 56 | .MuiTableContainer-root { 57 | margin: 0 auto; 58 | margin-top: 15rem; 59 | padding: 5px; 60 | } 61 | 62 | /* styling for kubeview container */ 63 | .kubeviewContainer { 64 | margin-top: 4rem; 65 | margin-left: 4rem; 66 | } 67 | 68 | /* styling for logo and form on signin/signup page */ 69 | .formWrapper .spyglass-logo { 70 | margin-top: 0; 71 | } 72 | .formWrapper { 73 | background: #232323; 74 | height: 100%; 75 | width: 100%; 76 | } 77 | 78 | /* link to sign up at bottom of form */ 79 | .formWrapper a { 80 | color: #fff; 81 | text-decoration: none; 82 | } 83 | .formWrapper a:hover { 84 | color: #0074d9; 85 | } 86 | 87 | /* styling for sign up form */ 88 | #username, 89 | #password, 90 | #IP-address, 91 | #password-label, 92 | #username-label, 93 | #IP-address-label { 94 | color: #fff; 95 | } 96 | .MuiAlert-icon .MuiSvgIcon-root { 97 | color: #ef5350; 98 | } -------------------------------------------------------------------------------- /src/client/components/dashboards/CostAnalysisDashboard.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import CostTable from '../CostTable'; 3 | 4 | const costURL = 5 | 'http://localhost:9090/model/allocation?aggregate=cluster&window=7d'; 6 | 7 | function CostAnalysisDashboard() { 8 | const [costData, setCostData] = useState({}); 9 | // initialize cost categories 10 | let totalCPU = 0; 11 | let totalRAM = 0; 12 | let totalPV = 0; 13 | // create function to access values associated with each type of cost and sum up totals 14 | const getCosts = (obj) => { 15 | for (const key in obj) { 16 | if (key === 'cpuCost') totalCPU += obj['cpuCost']; 17 | if (key === 'ramCost') totalRAM += obj['ramCost']; 18 | if (key === 'pvCost') totalPV += obj['pvCost']; 19 | } 20 | }; 21 | useEffect(() => { 22 | const fetchData = async () => { 23 | try { 24 | // fetch cost data from Kubecost 25 | const response = await fetch(costURL); 26 | const data = await response.json(); 27 | const costArray = data.data; 28 | // parse through fetched data 29 | costArray.forEach((obj) => { 30 | for (const cluster in obj) { 31 | getCosts(obj[cluster]); 32 | } 33 | }); 34 | const totalData = { 35 | totalCPU, 36 | totalRAM, 37 | totalPV 38 | }; 39 | // update state with new costsData 40 | setCostData(totalData); 41 | } catch (err) { 42 | // catch any errors 43 | console.log('error in fetching cost data: ', err); 44 | } 45 | }; 46 | // invoke async func fetchData defined above 47 | fetchData(); 48 | }, []); 49 | console.log('costData', costData); 50 | // render cost table only if we have successfuly retrieved data from Kubecost 51 | return ( 52 |
53 |

54 | {costData && ( 55 | 60 | )} 61 |
62 | ); 63 | } 64 | export default CostAnalysisDashboard; 65 | -------------------------------------------------------------------------------- /src/client/components/auth/AuthProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { authContext } from './authContext'; 4 | 5 | //provider component that wraps app and makes auth object returned by useProvideAuth 6 | //available to any child component that calls useAuth 7 | export function AuthProvider({ children }) { 8 | const auth = useProvideAuth(); 9 | return {children}; 10 | } 11 | 12 | //Provider hook creates auth object and handles state 13 | function useProvideAuth() { 14 | const [user, setUser] = useState(null); 15 | const navigate = useNavigate(); 16 | 17 | //signUp method creates user in DB, 18 | //if account is valid, update user in state and return user 19 | const signUp = async (credentials) => { 20 | const response = await fetch('http://localhost:3333/auth/signup', { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'Application/JSON' 24 | }, 25 | body: JSON.stringify(credentials) 26 | }); 27 | 28 | const newUser = await response.json(); 29 | 30 | if (typeof newUser === 'object') { 31 | setUser(newUser); 32 | return newUser; 33 | } else { 34 | return null; 35 | } 36 | }; 37 | //signin method verifies user 38 | //if account is valid, update user in state and return user 39 | const signIn = async (credentials) => { 40 | const response = await fetch('http://localhost:3333/auth/login', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'Application/JSON' 44 | }, 45 | body: JSON.stringify(credentials) 46 | }); 47 | 48 | const newUser = await response.json(); 49 | 50 | if (typeof newUser === 'object') { 51 | setUser(newUser); 52 | return newUser; 53 | } else { 54 | return null; 55 | } 56 | }; 57 | 58 | //signout, sets user to null and returns to signin page 59 | const signOut = () => { 60 | setUser(null); 61 | navigate('/signin', { replace: true }); 62 | }; 63 | 64 | //useProvideAuth return auth object and auth methods 65 | return { 66 | user, 67 | signUp, 68 | signIn, 69 | signOut 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /src/server/models/mongoooseModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | // package documentation: https://www.npmjs.com/package/connect-mongo 3 | import MongoStore from 'connect-mongo'; 4 | import bcrypt from 'bcrypt'; 5 | 6 | import * as dotenv from 'dotenv'; 7 | dotenv.config(); 8 | 9 | const URI = process.env.MONGO_URI; 10 | // define new database options 11 | const dbOptions = { 12 | useNewUrlParser: true, 13 | useUnifiedTopology: true 14 | }; 15 | 16 | // connect to database 17 | const dbConnection = await mongoose 18 | .connect(URI, dbOptions) 19 | .then(() => console.log('Database connected')) 20 | .catch((error) => console.log(error)); 21 | 22 | // create new session 23 | const sessionStore = MongoStore.create({ 24 | mongoUrl: URI, 25 | // When the session cookie has an expiration date, connect-mongo will use it 26 | ttl: 14 * 24 * 60 * 60, 27 | // name of collection for storing sessions 28 | collectionName: 'sessions', 29 | // will autoremove expired sessions. 30 | autoRemove: 'native' 31 | }); 32 | 33 | const Schema = mongoose.Schema; 34 | 35 | // create User schema 36 | const UserSchema = new Schema({ 37 | // define local schema so future groups can implement OAuth 38 | local: { 39 | username: { type: String, required: true }, 40 | password: { type: String, required: true } 41 | } 42 | }); 43 | 44 | // will hash password on User create 45 | UserSchema.pre('save', function (next) { 46 | const user = this; 47 | // generate a salt 48 | bcrypt.genSalt(8, function (err, salt) { 49 | if (err) return next(err); 50 | // hash the password using our new salt 51 | bcrypt.hash(user.local.password, salt, function (err, hash) { 52 | if (err) return next(err); 53 | // override the cleartext password with the hashed one 54 | user.local.password = hash; 55 | next(); 56 | }); 57 | }); 58 | }); 59 | 60 | // generate hashed password on register 61 | UserSchema.methods.generateHash = function (password) { 62 | return bcrypt.hash(password, bcrypt.genSalt(8), null); 63 | }; 64 | 65 | // checking if password is valid 66 | UserSchema.methods.validPassword = function (password) { 67 | return bcrypt.compareSync(password, this.local.password); 68 | }; 69 | 70 | export const User = mongoose.model('user', UserSchema); 71 | 72 | export { URI, dbConnection, sessionStore }; 73 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import session from 'express-session'; 3 | import passport from 'passport'; 4 | import cors from 'cors'; 5 | const app = express(); 6 | const PORT = 3333; 7 | 8 | // import config file and then configure passport 9 | import { passportConfig } from './config/passport.js'; 10 | passportConfig(passport); 11 | 12 | // ********** Import Routers ********** // 13 | import { authRouter } from './routes/authRoutes.js'; 14 | 15 | // ********** Import Session Store ********** // 16 | import { sessionStore } from './models/mongoooseModel.js'; 17 | 18 | app.use(cors()); 19 | 20 | // setting up passport.js and session implementation: 21 | // https://www.digitalocean.com/community/tutorials/easy-node-authentication-setup-and-local#toc-handling-signupregistration 22 | // https://youtu.be/J1qXK66k1y4 23 | 24 | // session initializer 25 | app.use( 26 | session({ 27 | secret: 'some secret', 28 | resave: false, 29 | saveUninitialized: true, 30 | store: sessionStore, 31 | cookie: { 32 | maxAge: 1000 * 60 * 60 * 24 33 | } 34 | }) 35 | ); 36 | 37 | // ********** initialize passport config to ensure user creds ********** // 38 | app.use(passport.initialize()); 39 | // ********** Related to express session middleware ********** // 40 | app.use(passport.session()); 41 | 42 | app.use(express.json()); 43 | app.use(express.urlencoded({ extended: true })); 44 | 45 | // ******** // 46 | // confirming that a session cookie is being set 47 | // Sessions not currently functional in auth. Use this for testing 48 | // app.get('/', function (req, res) { 49 | // res.send('Session info ' + JSON.stringify(req.session)); 50 | // }); 51 | // ******** // 52 | 53 | // ********** Authentication Router ********** // 54 | app.use('/auth', authRouter); 55 | 56 | // ********** Catch-all Err Handler ********** // 57 | app.use('*', (req, res) => res.status(404).json('ERROR 404: not found')); 58 | 59 | // ********** Global Err Handler ********** // 60 | app.use((err, req, res) => { 61 | const defaultErr = { 62 | log: 'Express error handler caught unknown middleware error', 63 | status: 500, 64 | message: { err: 'An error occurred' } 65 | }; 66 | const errorObj = { ...defaultErr, ...err }; 67 | console.log(errorObj.log); 68 | return res.status(errorObj.status).json(errorObj.message); 69 | }); 70 | 71 | app.listen(PORT, () => { 72 | console.log(`Server listening on port: ${PORT}...`); 73 | }); 74 | -------------------------------------------------------------------------------- /src/client/components/CostTable.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableContainer, 7 | TableHead, 8 | TableRow, 9 | Paper 10 | } from '@mui/material'; 11 | 12 | function CostTable({ totalCPU, totalRAM, totalPV }) { 13 | // calculate estimated monthly costs based data retrieved from Kubecost 14 | const monthlyCPU = totalCPU * 4; 15 | const monthlyRAM = totalRAM * 4; 16 | const monthlyPV = totalPV * 4; 17 | const monthlyTotal = monthlyCPU + monthlyRAM + monthlyPV; 18 | // create rows 19 | const createData = (name, cost) => { 20 | return { name, cost }; 21 | }; 22 | const rows = [ 23 | createData('CPU', '$' + monthlyCPU.toFixed(2)), 24 | createData('RAM', '$' + monthlyRAM.toFixed(2)), 25 | createData('PV', '$' + monthlyPV.toFixed(2)), 26 | createData('Total', '$' + monthlyTotal.toFixed(2)) 27 | ]; 28 | return ( 29 | 33 | 34 | {/* set heading in table*/} 35 | 36 | 37 | 41 | Cost Categories 42 | 43 | 47 | Total Costs Per Month 48 | 49 | 50 | 51 | {/* set rows in table*/} 52 | 53 | {rows.map((row) => ( 54 | 58 | 64 | {row.name} 65 | 66 | 67 | {row.cost} 68 | 69 | 70 | ))} 71 | 72 |
73 |
74 | ); 75 | } 76 | 77 | export default CostTable; 78 | -------------------------------------------------------------------------------- /src/client/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Drawer, 4 | ListItemText, 5 | List, 6 | ListItem, 7 | ListItemButton, 8 | ListItemIcon 9 | } from '@mui/material'; 10 | import BarChartIcon from '@mui/icons-material/BarChart'; 11 | import PriceChangeIcon from '@mui/icons-material/PriceChange'; 12 | import RemoveRedEyeIcon from '@mui/icons-material/RemoveRedEye'; 13 | import CloudIcon from '@mui/icons-material/Cloud'; 14 | import { Link } from 'react-router-dom'; 15 | import AnimatedLogo from './AnimatedLogo'; 16 | 17 | const drawerWidth = 290; 18 | function SideBar() { 19 | return ( 20 | // Drawer displays spyglass logo and links to various dashboards 21 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 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 | export default SideBar; 83 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // declare variables to import user-specific information from .env file 2 | const localClusterIP = import.meta.env.VITE_LOCALCLUSTERIP; 3 | const localClusterName = import.meta.env.VITE_LOCALCLUSTERNAME; 4 | const cloudClusterIP = import.meta.env.VITE_CLOUDCLUSTERIP; 5 | const cloudClusterName = import.meta.env.VITE_CLOUDCLUSTERNAME; 6 | 7 | // array of urls for local cluster (Minikube) metrics panels from Grafana 8 | // e.g. http://localhost:8000/d/IMOt5Yf4z/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=2 9 | const localDashboardURLs = [ 10 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=2`, 11 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=3`, 12 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=5`, 13 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=6`, 14 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=7`, 15 | `http://${localClusterIP}/d/${localClusterName}/node-exporter-nodes?orgId=1&refresh=30s&viewPanel=8` 16 | ]; 17 | 18 | // array of urls for cloud cluster (AWS) metrics panels from Grafana 19 | // e.g. http://a5f23f08f01e34e6c883489e8cfef487-101927145.us-east-1.elb.amazonaws.com/d/bgPQC9f4z/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680212302956&to=1680213202956&viewPanel=32 20 | const cloudDashboardURLs = [ 21 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211308893&to=1680212208893&viewPanel=32`, 22 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211595011&to=1680212495011&viewPanel=4`, 23 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211712351&to=1680212612351&viewPanel=24`, 24 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211731333&to=1680212631333&viewPanel=28`, 25 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211731333&to=1680212631333&viewPanel=30`, 26 | `http://${cloudClusterIP}/d/${cloudClusterName}/kubernetes-cluster-monitoring-via-prometheus?orgId=1&refresh=10s&from=1680211224252&to=1680212124252&viewPanel=6` 27 | ]; 28 | 29 | export { localDashboardURLs, cloudDashboardURLs }; 30 | -------------------------------------------------------------------------------- /src/server/config/passport.js: -------------------------------------------------------------------------------- 1 | import passportLocal from 'passport-local'; 2 | const LocalStrategy = passportLocal.Strategy; 3 | 4 | import { User } from '../models/mongoooseModel.js'; 5 | 6 | // passport config function 7 | export const passportConfig = function (passport) { 8 | // passport session setup required for persistent login sessions passport needs ability to serialize and unserialize users out of session 9 | passport.serializeUser(function (user, done) { 10 | done(null, user.id); 11 | }); 12 | 13 | passport.deserializeUser(async function (id, done) { 14 | try { 15 | const user = await User.findById(id); 16 | return done(null, user); 17 | } catch (err) { 18 | return done(err); 19 | } 20 | }); 21 | 22 | // LOCAL SIGNUP authentication 23 | passport.use( 24 | 'local-signup', 25 | new LocalStrategy( 26 | { 27 | // by default, local strategy uses username and password, we will override with email 28 | usernameField: 'username', 29 | passwordField: 'password', 30 | passReqToCallback: true // allows us to pass back the entire request to the callback 31 | }, 32 | function (req, username, password, done) { 33 | // asynchronous User.findOne wont fire unless data is sent back 34 | process.nextTick(async function () { 35 | try { 36 | const userCheck = await User.findOne({ 37 | 'local.username': username 38 | }); 39 | if (userCheck) return done(null, false); 40 | else { 41 | const addUser = await User.create({ 42 | local: { username: username, password: password } 43 | }); 44 | return done(null, addUser); 45 | } 46 | } catch (err) { 47 | return done(err); 48 | } 49 | }); 50 | } 51 | ) 52 | ); 53 | 54 | // LOCAL LOGIN authentication 55 | passport.use( 56 | 'local-login', 57 | new LocalStrategy( 58 | { 59 | // by default, local strategy uses username and password, we will override with email 60 | usernameField: 'username', 61 | passwordField: 'password', 62 | passReqToCallback: true // allows us to pass back the entire request to the callback 63 | }, 64 | async function (req, username, password, done) { 65 | try { 66 | const checkUser = await User.findOne({ 'local.username': username }); 67 | if (!checkUser) return done(null, false); 68 | if (!checkUser.validPassword(password)) return done(null, false); 69 | return done(null, checkUser); 70 | } catch (err) { 71 | return done(err); 72 | } 73 | } 74 | ) 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/client/pages/Signup.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import Box from '@mui/material/Box'; 7 | import Container from '@mui/material/Container'; 8 | import AnimatedLogo from '../components/AnimatedLogo'; 9 | import { useAuth } from '../components/auth/useAuth'; 10 | import { useNavigate } from 'react-router-dom'; 11 | import Alert from '@mui/material/Alert'; 12 | 13 | function SignUp() { 14 | const auth = useAuth(); 15 | const [loginFail, setLoginFail] = useState(false); 16 | const navigate = useNavigate(); 17 | 18 | const handleSubmit = async (event) => { 19 | event.preventDefault(); 20 | const formData = new FormData(event.currentTarget); 21 | const username = formData.get('username'); 22 | const password = formData.get('password'); 23 | const authedUser = await auth.signUp({ username, password }); 24 | if (authedUser) { 25 | navigate('/', { replace: true }); 26 | } else { 27 | setLoginFail(true); 28 | } 29 | }; 30 | 31 | return ( 32 |
33 | 34 | 35 | 47 | 48 | 55 | 60 | Invalid username or password. Please retry. 61 | 62 | 72 | 82 | 90 | 91 | 92 | 93 |
94 | ); 95 | } 96 | 97 | export default SignUp; 98 | -------------------------------------------------------------------------------- /src/client/pages/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | import Button from '@mui/material/Button'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import TextField from '@mui/material/TextField'; 6 | import Grid from '@mui/material/Grid'; 7 | import Box from '@mui/material/Box'; 8 | import Container from '@mui/material/Container'; 9 | import { Link, useNavigate } from 'react-router-dom'; 10 | import { useAuth } from '../components/auth/useAuth'; 11 | import AnimatedLogo from '../components/AnimatedLogo'; 12 | import Alert from '@mui/material/Alert'; 13 | 14 | function SignIn() { 15 | const auth = useAuth(); 16 | const [loginFail, setLoginFail] = useState(false); 17 | const navigate = useNavigate(); 18 | 19 | const handleSubmit = async (event) => { 20 | event.preventDefault(); 21 | const formData = new FormData(event.currentTarget); 22 | const username = formData.get('username'); 23 | const password = formData.get('password'); 24 | const authedUser = await auth.signIn({ username, password }); 25 | if (authedUser) { 26 | navigate('/', { replace: true }); 27 | } else { 28 | setLoginFail(true); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 | 35 | 36 | 48 | 49 | 55 | 60 | Invalid username or password. Please retry. 61 | 62 | 72 | 82 | 90 | 91 | 92 | 93 | {"Don't have an account?"} 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | ); 102 | } 103 | 104 | export default SignIn; 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Spyglass 2 | 3 | ## What is Spyglass? 4 | 5 | Spyglass is an open-source tool that allows users to monitor Kubernetes cluster metrics and track cluster deployment costs in a centralized location. 6 | Spyglass is actively being developed with the support of OSLabs and we are always looking for contributors and feedback. 7 | 8 | Check out our [website](https://spyglass-website.vercel.app/)! 9 |
10 | 11 |
12 | 13 | 14 | [![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://reactjs.org/) 15 | [![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 16 | [![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white)](https://grafana.com/) 17 | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://kubernetes.io/) 18 | [![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white)](https://prometheus.io/) 19 | [![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white)](https://www.mongodb.com/) 20 | [![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white)](https://mui.com/) 21 | [![License](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](public/LICENSE) 22 | 23 |
24 | 25 |
26 | 27 | ## Features 28 | 29 | - Monitor cluster performance and cost analysis in a centralized dashboard 30 | - Visualize clusters and resources with an intuitive and user-friendly interface provided by Kubeview 31 | - Analyze key metrics related to cluster performance with a suite of detailed charts, graphs, and other visualized data powered by Prometheus and Grafana 32 | - Efficiently manage Kubernetes expenses with monthly cost projections powered by Kubecost 33 | 34 |
35 | 36 | ## Getting Started 37 | 38 | Check out our detailed [setup](/SETUP.md) guide to get started! 39 | 40 |
41 | 42 | ## Iteration Plans 43 | 44 | - Implement unit and end-to-end testing 45 | - Migrate the rest of the codebase to Typescript 46 | - Manage sessions with user authentication 47 | - Create an alert manager that sends user notifications 48 | - Configure Prometheus deployment to provide customized metrics 49 | - Develop passport authentication for SQL database 50 | - Create Makefile for faster setup 51 |
52 | 53 | ## Connect with the Team 54 | 55 | Feel free to reach out to us with any questions or feedback! 56 | | Cindy Chau | Alex Czaja | Easton Miller | Anthony Vega | 57 | | :---: | :---: | :---: | :---: | 58 | | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/cindychau1) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/cindychau11/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/aczaja85) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/alex-czaja/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/jEastonMiller) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/j-easton-miller/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/anthonyrvega) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/anthony-r-vega/) | 59 | 60 |
61 | 62 | ## Show Your Support 63 | 64 | If you like this project, please give it a ⭐️! 65 | 66 |
67 | 68 | ## License 69 | 70 | By contributing, you agree that your contributions will be licensed under its [MIT License](/LICENSE). 71 | -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | ## Spyglass' Setup Instructions 2 | 3 | Clone the Spyglass repo from GitHub to your local machine. 4 | ``` 5 | git clone https://github.com/oslabs-beta/spyglass.git 6 | ``` 7 | 8 | ## Create a .env file with: 9 | ``` 10 | MONGO_URI= 11 | VITE_LOCALCLUSTERIP= 12 | VITE_LOCALCLUSTERNAME= 13 | VITE_CLOUDCLUSTERIP= 14 | VITE_CLOUDCLUSTERNAME= 15 | ``` 16 | 17 | Here is an example ENV file: 18 | ``` 19 | MONGO_URI= "mongodb+srv://dummyAcc:HVcsVJuNv2fTCJJl@spyglassdev.jmhr4fn.mongodb.net/?retryWrites=true&w=majority" 20 | VITE_LOCALCLUSTERIP=localhost:8000 21 | VITE_LOCALCLUSTERNAME=IMOt5Yf4z 22 | VITE_CLOUDCLUSTERIP=aefc1187804224b2389464585a69932b-1354669704.us-west-2.elb.amazonaws.com 23 | VITE_CLOUDCLUSTERNAME=DtgSFtBVk 24 | ``` 25 | 26 |
27 | 28 | ## Deploy a local Kubernetes cluster on Minikube 29 | To get started, you will need to have a Kubernetes cluster. You can create a single-node Kubernetes cluster on your local machine using Minikube. Install Minikube using [documentation](https://minikube.sigs.k8s.io/docs/start/). 30 | 31 | Here are instructions if you have a MacOS: 32 | 33 | 1. Install Docker 34 | ``` 35 | brew install docker 36 | ``` 37 | 38 | 2. Install Minikube 39 | ``` 40 | curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64 41 | sudo install minikube-darwin-arm64 /usr/local/bin/minikube 42 | 43 | ``` 44 | 45 | 3. Start your cluster 46 | ``` 47 | minikube start --vm-driver=docker 48 | ``` 49 | 50 |
51 | 52 | ## Install Helm and Kube-Prometheus-Stack 53 | Helm is a package manager for Kubernetes that manages and packages all the necessary resources for your Kubernetes cluster in a single unit called a chart. See [documentation](https://helm.sh/docs/intro/quickstart/) for more information. 54 | 55 | Kube-Prometheus-Stack is a Helm chart that includes a set of applications to monitor Kubernetes clusters using the Prometheus monitoring system. See [documentation](https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/README.md) for more information. 56 | 57 | Here are instructions if you have a MacOS: 58 | 59 | 1. Install Helm 60 | ``` 61 | brew install helm 62 | ``` 63 | 64 | 2. Add prometheus-community repo to Helm and update 65 | ``` 66 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 67 | helm repo update 68 | ``` 69 | 70 | 3. Create a new namespace in your cluster named ```monitoring``` 71 | ``` 72 | kubectl create namespace monitoring 73 | ``` 74 | 75 | 4. Install Kube-Prometheus-Stack 76 | ``` 77 | helm install kubepromstack prometheus-community/kube-prometheus-stack --namespace=monitoring 78 | ``` 79 | 80 | 5. Retrieve information about the pods running in the ```monitoring``` namespace of your Kubernetes cluster 81 | ``` 82 | kubectl get pods --namespace monitoring 83 | ``` 84 |
85 | 86 | 87 | ## Access Grafana for cluster health metrics 88 | Grafana is an application part of Kube-Prometheus-Stack and provides visualizations for metrics monitoring a Kubernetes cluster. See [documentation](https://grafana.com/grafana/) for more information. 89 | 90 | 1. Edit Grafana's configuration map in order to render visuals correctly in Spyglass. 91 | ``` 92 | kubectl edit configmap kubepromstack-grafana --namespace monitoring 93 | ``` 94 | 95 | Make this change in Grafana's configuration map. 96 | ``` 97 | [security] 98 | allow_embedding = true 99 | ``` 100 | 101 | 2. Access Grafana by port-forwarding to http://localhost:8000 or click "Local Cluster Metrics" in Spyglass. 102 | ``` 103 | kubectl port-forward -n monitoring svc/kubepromstack-grafana 8000:80 104 | ``` 105 | 106 | ## Access Kubecost for cost optimization 107 | Kubecost analyzes CPU, PV, and RAM resource usage on Kubernetes clusters. Using Kubecost, Spyglass helps provide monthly estimates to help you optimize your costs! See [documentation](https://docs.kubecost.com/) for more information. 108 | 109 | 1. Install Kubecost and create a namespace named ```kubecost``` 110 | ``` 111 | helm install kubecost cost-analyzer \ 112 | --repo https://kubecost.github.io/cost-analyzer/ \ 113 | --namespace kubecost --create-namespace \ 114 | --set kubecostToken="Y2luZHljaGF1MTFAZ21haWwuY29txm343yadf98" 115 | ``` 116 | 117 | 2. Retrieve information about the pods running in the ```kubecost``` namespace of your Kubernetes cluster 118 | ``` 119 | kubectl get pods --namespace kubecost 120 | ``` 121 | 122 | 3. Access Kubecost by port-forwarding to http://localhost:9090 or click "Cost Analysis" in Spyglass. 123 | ``` 124 | kubectl port-forward --namespace kubecost deployment/kubecost-cost-analyzer 9090 125 | ``` 126 | 127 |
128 | 129 | ## Access Kubeview for cluster visualization 130 | Kubeview provides a graphical representation of a Kubernetes cluster and its resources. See [documentation](https://github.com/benc-uk/kubeview) for more information. 131 | 132 | 1. Add Kubeview repo to Helm 133 | ``` 134 | helm repo add kubeview https://benc-uk.github.io/kubeview/charts 135 | ``` 136 | 137 | 2. Install Kubeview (Please replace with current version) 138 | ``` 139 | helm install my-kubeview kubeview/kubeview --version 0.1.31 --namespace=monitoring 140 | ``` 141 | 142 | 3. Access Kubeview by port-forwarding to http://localhost:9000 or click "Cluster Visualizer" in Spyglass. 143 | ``` 144 | kubectl port-forward svc/my-kubeview -n monitoring 9000:80 145 | ``` 146 | 147 | ## Access Prometheus and make custom PROMQL queries 148 | Prometheus is another application part of Kube-Prometheus-Stack and scrapes metrics on Kubernetes clusters. See [documentation](https://prometheus.io/docs/prometheus/latest/getting_started/) for more information. 149 | 150 | 1. Access Prometheus by port-forwarding to http://localhost:7000 151 | ``` 152 | kubectl port-forward -n monitoring svc/kubepromstack-prometheus 7000:9090 153 | ``` --------------------------------------------------------------------------------