├── 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 |
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 |
18 | Sign Out
19 |
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 |
88 | Sign up
89 |
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 |
88 | Sign In
89 |
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 | [](https://reactjs.org/)
15 | [](https://www.typescriptlang.org/)
16 | [](https://grafana.com/)
17 | [](https://kubernetes.io/)
18 | [](https://prometheus.io/)
19 | [](https://www.mongodb.com/)
20 | [](https://mui.com/)
21 | [](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 | | [](https://github.com/cindychau1) [](https://www.linkedin.com/in/cindychau11/) | [](https://github.com/aczaja85) [](https://www.linkedin.com/in/alex-czaja/) | [](https://github.com/jEastonMiller) [](https://www.linkedin.com/in/j-easton-miller/) | [](https://github.com/anthonyrvega) [](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 | ```
--------------------------------------------------------------------------------