├── .dockerignore ├── .gitignore ├── README.md ├── __tests__ ├── AuthRouter.test.js └── server.test.js ├── app.json ├── assets ├── fylakas-logo-export.png ├── readme │ ├── CONNECT.gif │ ├── DEPLOY.png │ └── GRAPHS.gif └── styles.css ├── client ├── App.jsx ├── authComponents │ ├── LoginBox.jsx │ ├── LoginPage.jsx │ └── SignupBox.jsx ├── dashboardComponents │ ├── AlertBox.jsx │ ├── ClusterHealthHeader.jsx │ ├── ComparisonTab.jsx │ ├── Connection.jsx │ ├── CurrentHealthTab.jsx │ ├── DashboardPage.jsx │ ├── ErrorBox.jsx │ ├── LoadingPage.jsx │ ├── MetricModal.jsx │ ├── PageMode.jsx │ ├── PastHealthTab.jsx │ ├── Sidebar.jsx │ ├── StatBox.jsx │ ├── VisualizerBox.jsx │ ├── statBoxComponents │ │ ├── ContainerRunningBox.jsx │ │ ├── NodesRunningBox.jsx │ │ └── PodsRunningBox.jsx │ └── visualizerComponents │ │ └── VisualizationItem.jsx └── index.html ├── package-lock.json ├── package.json ├── postgres_create.sql ├── readme-assets └── Screen Shot 2023-09-26 at 12.26.33 PM.png ├── server ├── controllers │ ├── authController.js │ ├── k8sController.js │ ├── postgraphileController.js │ └── promController.js ├── db │ └── database.js ├── models │ └── UserModel.js ├── routers │ ├── K8srouter.js │ ├── authRouter.js │ └── promRouter.js └── server.js └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.classpath 8 | **/.dockerignore 9 | **/.env 10 | **/.git 11 | **/.gitignore 12 | **/.project 13 | **/.settings 14 | **/.toolstarget 15 | **/.vs 16 | **/.vscode 17 | **/.next 18 | **/.cache 19 | **/*.*proj.user 20 | **/*.dbmdl 21 | **/*.jfm 22 | **/charts 23 | **/docker-compose* 24 | **/compose* 25 | **/Dockerfile* 26 | **/node_modules 27 | **/npm-debug.log 28 | **/obj 29 | **/secrets.dev.yaml 30 | **/values.dev.yaml 31 | **/build 32 | **/dist 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fylakas 2 | 3 | Fylakas is a Kubernetes cluster monitoring and visualization tool designed to provide helpful metrics and insight into the health of your cluster. 4 | 5 | ## Table of Contents 6 | 7 | 1. [Getting Started](#Getting-Started) 8 | 2. [Contributing](#Contributing) 9 | 3. [Progress](#Progress) 10 | 4. [Scripts](#Scripts) 11 | 5. [Our Team](#our-team) 12 | 6. [License](#license) 13 | 14 | ## Getting Started 15 | 16 | Fylakas expects users to have a preconfigured Prometheus server deployed within their cluster in order to provide metrics. If you'd like assistance with setting up a Kubernetes cluster or with deploying Kubernetes within that cluster, [this article](https://devopscube.com/setup-prometheus-monitoring-on-kubernetes/) is a great place to start. 17 | 18 | 1. **Connect To Your Cluster**: Ensure that both your application and your Prometheus server are deployed within your cluster by using the CLI command `kubectl get all`. The result should look something like this: 19 | ![kubectl get all GIF](./assets/readme/DEPLOY.png) 20 | If either Prometheus or your application are not deployed within the cluster, use the link above to determine how to deploy your application and / or Prometheus within your Kubernetes cluster. 21 | 22 | 2. **Connect Your Prometheus Server to Fylakas**: On the dashboard page, within the form labeled "CONNECT," input and submit the URL to your Prometheus Server. Under the hood, this will be initialized to a variable which tells our PromController where to send the queries for the metrics you want. The value of the server URL defaults to [http://localhost:9090](http://localhost:9090). 23 | 24 | ![Connect To Prom Server GIF](./assets/readme/CONNECT.gif) 25 | 26 | 3. **See Your Data**: Once connected, your data should be visualized within the graphics on the dashboard page. Fylakas is configured to make a request to the Prometheus Server every 15 seconds. You can configure the interval that the Prometheus Server will scrape the data by locating or creating a `prometheus.yaml` file and assigning the desired `scrape_configs`. For example: 27 | 28 | scrape_configs: 29 | 30 | - job_name: 'example-job' 31 | static_configs: 32 | - targets: ['example.com:9090'] # Replace with your target's address and port 33 | scrape_interval: 10s # Set the scrape interval to 10 seconds 34 | scrape_timeout: 5s # Set the scrape timeout to 5 seconds 35 | 36 | ![Graphs GIF](./assets/readme/GRAPHS.gif) 37 | 38 | ## Contributing 39 | 40 | Fylakas is proud to be an Open Source Product. Contributions and additions to the product are not only permitted, but encouraged! 41 | 42 | If you wish to contribute and / or be part of the team, please follow the following guidelines: 43 | 44 | 1. Fork and clone the repository 45 | 2. Branch off of the dev branch to create your own feature branch 46 | - The Feature branch name should start with feat, fix, bug, docs, test, wip, or merge (e.g. feat/database) 47 | 3. Commit your changes (git commit -m '(feat/bugfix/style/etc.): [commit message here]') 48 | - Please review [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) standards for more information 49 | 4. Once the feature is built and the commit is properly configured, submit a pull request to dev 50 | 51 | ## Progress 52 | 53 | | Feature | Status | 54 | | ------------------------------ | ------ | 55 | | Cluster Data Visualization | ✅ | 56 | | Application Data Visualization | ⏳ | 57 | | Customizable Visualization | ⏳ | 58 | | Predictive Visualization Tool | 🙏🏻 | 59 | 60 | - ✅ = Ready to use 61 | - ⏳ = In progress 62 | - 🙏🏻 = Looking for contributors 63 | 64 | ## Scripts 65 | 66 | Below are descriptions of each npm script: 67 | 68 | - `npm run build`: Starts the build mode 69 | - `npm start`: Starts the production server using Nodemon 70 | - `npm run dev`: Starts the development server using Nodemon 71 | - `npm run test`: Runs tests with jest 72 | 73 | ## Our Team 74 | 75 | Quinn Graves 76 | [LinkedIn](https://www.linkedin.com/in/quinn-graves-84673028a/) 77 | [GitHub](https://github.com/qpgdev) 78 | 79 | Nathan Gonsalves 80 | [LinkedIn](https://www.linkedin.com/in/iamkaprekar) 81 | [GitHub](https://github.com/iAmKaprekar) 82 | 83 | Katherine Fry 84 | [LinkedIn](https://www.linkedin.com/in/katherine-fry-49071021b) 85 | [GitHub](https://github.com/KatFry) 86 | 87 | Bogdana Oliynyk 88 | [LinkedIn](https://www.linkedin.com/in/bogdanaoliynyk/) 89 | [GitHub](https://github.com/Bogdana-Oliynyk) 90 | 91 | Sebastian Salazar 92 | [LinkedIn](https://www.linkedin.com/in/sebastian-salazar-526b75284/) 93 | [GitHub](https://github.com/razalas340) 94 | 95 | ## License 96 | 97 | By contributing, you agree that your contributions will be licensed under Fylakas' MIT License. 98 | -------------------------------------------------------------------------------- /__tests__/AuthRouter.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { app } = require('../server/server'); 3 | let server; // Declare server variable 4 | const db = require('../server/db/database.js'); // Importing the pool to then close it in after all 5 | 6 | beforeAll(() => { 7 | process.env.PORT = 3001; 8 | const serverModule = require('../server/server'); 9 | server = serverModule.server || serverModule.app.listen(process.env.PORT); // Start the server here if it hasn't started yet 10 | }); 11 | 12 | describe('Auth Routes', () => { 13 | // Test for /login 14 | describe('POST /auth/login', () => { 15 | it('should login with valid credentials', async () => { 16 | const response = await request(app) 17 | .post('/api/auth/login') 18 | .send({ username: 'testuser', password: 'testpassword' }); // Adjust with actual credentials 19 | expect(response.statusCode).toBe(200); 20 | expect(response.body.profile).toBeDefined(); 21 | }); 22 | }); 23 | }); 24 | 25 | // Test for /signup 26 | describe('POST /auth/signup', () => { 27 | it('should signup a new user', async () => { 28 | const response = await request(app) 29 | .post('/api/auth/signup') 30 | .send({ username: 'newuser', password: 'newpassword' }); // Adjust with actual credentials 31 | expect(response.statusCode).toBe(200); 32 | expect(response.body.profile).toBeDefined(); 33 | }); 34 | }); 35 | 36 | describe('GET /check', () => { 37 | it('should return a status indicating if the user is logged in', async () => { 38 | const response = await request(app).get('/api/auth/check'); 39 | 40 | // This is a generic check, adapt based on your expected response format and logic 41 | expect(response.status).toBe(200); 42 | expect(response.body).toHaveProperty('isLoggedIn'); 43 | expect(typeof response.body.isLoggedIn).toBe('boolean'); 44 | }); 45 | }); 46 | 47 | describe('POST /logout', () => { 48 | it('should log the user out and return a success status', async () => { 49 | const response = await request(app).post('/api/auth/logout'); 50 | 51 | // Again, adapt based on your expected response format and logic 52 | expect(response.status).toBe(200); 53 | expect(response.body).toHaveProperty('message', 'Successfully logged out'); 54 | }); 55 | }); 56 | 57 | afterAll(async () => { 58 | if (server) server.close(); // Close the server after all tests 59 | await db.end(); // Close the pool 60 | }); 61 | -------------------------------------------------------------------------------- /__tests__/server.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const { app } = require('../server/server'); 3 | let server; // Declare server variable 4 | 5 | beforeAll(() => { 6 | process.env.PORT = 3000; 7 | const serverModule = require('../server/server'); 8 | server = serverModule.server || serverModule.app.listen(process.env.PORT); // Start the server here if it hasn't started yet 9 | }); 10 | 11 | describe('Server Routes', () => { 12 | describe('GET /', () => { 13 | it('should respond with the index.html file', async () => { 14 | const response = await request(app).get('/'); 15 | expect(response.statusCode).toBe(200); 16 | expect(response.type).toBe('text/html'); 17 | }); 18 | }); 19 | 20 | describe('Unknown Routes', () => { 21 | it('should respond with a 404 status and message', async () => { 22 | const response = await request(app).get('/unknownpath'); 23 | expect(response.statusCode).toBe(404); 24 | expect(response.text).toBe("This is not the page you're looking for..."); 25 | }); 26 | }); 27 | }); 28 | 29 | afterAll((done) => { 30 | if (server) server.close(done); // Close the server after all tests 31 | }); 32 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Start on Heroku: Node.js", 3 | "description": "fylakas", 4 | "repository": "https://github.com/oslabs-beta/fylakas", 5 | "logo": "./assets/fylakas-logo-export.png", 6 | "keywords": ["node", "express", "heroku"], 7 | "image": "heroku/nodejs" 8 | } -------------------------------------------------------------------------------- /assets/fylakas-logo-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/fylakas/b5d75c735ee6fd9215e4a675de580f2c44dbbfcd/assets/fylakas-logo-export.png -------------------------------------------------------------------------------- /assets/readme/CONNECT.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/fylakas/b5d75c735ee6fd9215e4a675de580f2c44dbbfcd/assets/readme/CONNECT.gif -------------------------------------------------------------------------------- /assets/readme/DEPLOY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/fylakas/b5d75c735ee6fd9215e4a675de580f2c44dbbfcd/assets/readme/DEPLOY.png -------------------------------------------------------------------------------- /assets/readme/GRAPHS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/fylakas/b5d75c735ee6fd9215e4a675de580f2c44dbbfcd/assets/readme/GRAPHS.gif -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css'); 2 | 3 | .row { 4 | --bs-gutter-x: 1.5rem; 5 | --bs-gutter-y: 0; 6 | display: flex; 7 | flex-wrap: wrap; 8 | margin-top: calc(-1 * var(--bs-gutter-y)); 9 | margin-right: calc(-0.5 * var(--bs-gutter-x)); 10 | margin-left: calc(-0.5 * var(--bs-gutter-x)); 11 | } 12 | 13 | :root[data-theme='light'] { 14 | background-color: #ffffff; /* Light mode background color */ 15 | } 16 | 17 | :root[data-theme='dark'] { 18 | background-color: #333333; /* Dark mode background color */ 19 | } 20 | 21 | /* Define the styles for the dropdown and menu 22 | #bd-theme { 23 | /* Add your styles for the button here */ 24 | /* } */ 25 | 26 | /* #bd-theme-text::after { */ 27 | /* Add any styles for the icon after the text here */ 28 | /* } */ 29 | 30 | .dropdown-menu { 31 | /* Add styles for the dropdown menu here */ 32 | --bs-dropdown-bg: #4a90e2; 33 | --bs-dropdown-link-active-bg: #357ca5; 34 | } 35 | 36 | /* Define styles for each theme using the data-bs-theme attribute */ 37 | [data-bs-theme='light'] { 38 | background-color: #ffffff; /* Light mode background color */ 39 | } 40 | 41 | [data-bs-theme='dark'] { 42 | background-color: #333333; /* Dark mode background color */ 43 | } 44 | 45 | .btn-bd-primary { 46 | --bd-violet-bg: #712cf9; 47 | --bd-violet-rgb: 112.520718, 44.062154, 249.437846; 48 | --bs-btn-font-weight: 600; 49 | --bs-btn-color: var(--bs-white); 50 | --bs-btn-bg: var(--bd-violet-bg); 51 | --bs-btn-border-color: var(--bd-violet-bg); 52 | --bs-btn-hover-color: var(--bs-white); 53 | --bs-btn-hover-bg: #6528e0; 54 | --bs-btn-hover-border-color: #6528e0; 55 | --bs-btn-focus-shadow-rgb: var(--bd-violet-rgb); 56 | --bs-btn-active-color: var(--bs-btn-hover-color); 57 | --bs-btn-active-bg: #5a23c8; 58 | --bs-btn-active-border-color: #5a23c8; 59 | } 60 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import DashboardPage from './dashboardComponents/DashboardPage.jsx'; 3 | import LoginPage from './authComponents/LoginPage.jsx'; 4 | import LoadingPage from './dashboardComponents/LoadingPage.jsx'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | function App() { 8 | // declare initial state and assign to 'loading' using useState hook 9 | const [isLoggedIn, setIsLoggedIn] = useState('loading'); 10 | 11 | useEffect(() => { 12 | // Will check a jwt cookie to see if any user is logged in. 13 | fetch('api/auth/check') 14 | .then((response) => { 15 | if (response.ok) return response.json(); 16 | throw new Error('ERROR: request in App useEffect failed'); 17 | }) 18 | .then((response) => { 19 | console.log(response); 20 | // Change state of isLoggedIn to match the response given 21 | response.isLoggedIn ? setIsLoggedIn(true) : setIsLoggedIn(false); 22 | }); 23 | }, []); 24 | 25 | // Load page "loading" initially. 26 | // If logged in, serve the Dashboard page. If not, serve the Login page. 27 | const page = 28 | isLoggedIn === 'loading' ? ( 29 | 30 | ) : isLoggedIn ? ( 31 | setIsLoggedIn(false)} /> 32 | ) : ( 33 | setIsLoggedIn(true)} /> 34 | ); 35 | 36 | // Render the page within a div 37 | return
{page}
; 38 | } 39 | 40 | // Render the application inside the root div. 41 | const root = createRoot(document.querySelector('#root')); 42 | root.render(); 43 | -------------------------------------------------------------------------------- /client/authComponents/LoginBox.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | const LoginBox = ({ routeToSignupPage, logIn }) => { 4 | 5 | // Declare state for field input strings and error booleans. 6 | const [usernameField, setUsernameField] = useState(''); 7 | const [passwordField, setPasswordField] = useState(''); 8 | const [usernameError, setUsernameError] = useState(false); 9 | const [passwordError, setPasswordError] = useState(false); 10 | const [loginError, setLoginError] = useState(false); 11 | 12 | // Error boxes can be empty if there is no error, or filled with the appropriate error message if needbe. 13 | const usernameErrorBox = usernameError ?
You must enter a username.
: <>; 14 | const passwordErrorBox = passwordError ?
You must enter a password.
: <>; 15 | const loginErrorBox = loginError ?
Invalid login credentials. Please check the spelling of your username and password.
: <>; 16 | 17 | const checkLogIn = (e) => { 18 | // Prevent the page from reloading on event. 19 | e.preventDefault(); 20 | // Reset all the state pertaining to errors. 21 | if (usernameError) setUsernameError(false); 22 | if (passwordError) setPasswordError(false); 23 | if (loginError) setLoginError(false); 24 | // If both a username and password have been entered, make a login request. 25 | if (usernameField && passwordField) { 26 | fetch('api/auth/login', { 27 | method: 'POST', 28 | headers: {'Content-Type': 'application/json'}, 29 | body: JSON.stringify({username: usernameField, password: passwordField}) 30 | }) 31 | .then(response => { 32 | if (response.ok) return response.json(); 33 | throw new Error('ERROR: request in checkLogIn failed'); 34 | }) 35 | .then(response => { 36 | // If a profile is returned, set state isLoggedIn to true. 37 | // If not, flag the loginError as true. 38 | if (response.profile) { 39 | logIn(); 40 | } else { 41 | setLoginError(true); 42 | } 43 | }) 44 | .catch(err => console.log(err)); 45 | } else { 46 | // Determine which fields need to be filled and flag the appropriate error. 47 | if (!usernameField) setUsernameError(true); 48 | if (!passwordField) setPasswordError(true); 49 | } 50 | } 51 | 52 | return ( 53 |
54 |
55 |
56 | 57 |

Welcome Aboard

58 |
59 | {usernameErrorBox} 60 | {loginErrorBox} 61 | setUsernameField(e.target.value)} 68 | > 69 | 70 |
71 |
72 | {passwordErrorBox} 73 | setPasswordField(e.target.value)} 80 | > 81 | 82 |
83 |
84 | 85 | 86 |
87 |
88 | ) 89 | } 90 | 91 | export default LoginBox; -------------------------------------------------------------------------------- /client/authComponents/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import LoginBox from './LoginBox.jsx'; 4 | import SignupBox from './SignupBox.jsx'; 5 | 6 | const LoginPage = ({logIn}) => { 7 | 8 | // State initialization shows user LoginBox on initial loadup 9 | const [accountExists, setAccountExists] = useState(true); 10 | 11 | // Determines whether to show the LoginBox or SignupBox through events passed to each 12 | const page = accountExists ? 13 | setAccountExists(false)} logIn = {logIn}/> : 14 | setAccountExists(true)} logIn = {logIn}/>; 15 | 16 | return ( 17 |
18 | {page} 19 |
20 | ) 21 | } 22 | 23 | export default LoginPage; -------------------------------------------------------------------------------- /client/authComponents/SignupBox.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | const SignupBox = ({ routeToLoginPage, logIn }) => { 4 | 5 | // Declare state for field input strings and error booleans. 6 | const [usernameField, setUsernameField] = useState(''); 7 | const [passwordField, setPasswordField] = useState(''); 8 | const [usernameError, setUsernameError] = useState(false); 9 | const [passwordError, setPasswordError] = useState(false); 10 | const [signupError, setSignupError] = useState(false); 11 | 12 | // Error boxes can be empty if there is no error, or filled with the appropriate error message if needbe. 13 | const usernameErrorBox = usernameError ?
You must enter a username.
: <>; 14 | const passwordErrorBox = passwordError ?
You must enter a password.
: <>; 15 | const signupErrorBox = signupError ?
The username you selected already belongs to an account.
: <>; 16 | 17 | const signUp = (e) => { 18 | // Prevent the page from reloading on event. 19 | e.preventDefault(); 20 | // Reset all the state pertaining to errors. 21 | if (signupError) setSignupError(false); 22 | if (usernameError) setUsernameError(false); 23 | if (passwordError) setPasswordError(false); 24 | // If both a username and password have been entered, make a login request. 25 | if (usernameField && passwordField) { 26 | fetch('api/auth/signup', { 27 | method: 'POST', 28 | headers: {'Content-Type': 'application/json'}, 29 | body: JSON.stringify({username: usernameField, password: passwordField}) 30 | }) 31 | .then(response => { 32 | if (response.ok) return response.json(); 33 | throw new Error('ERROR: request in checkLogIn failed'); 34 | }) 35 | .then(response => { 36 | // If a profile is returned, set state isLoggedIn to true. 37 | // If not, flag the signupError as true. 38 | if (response.profile) { 39 | logIn(); 40 | } else { 41 | setSignupError(true); 42 | } 43 | }) 44 | .catch(err => console.log(err)); 45 | } else { 46 | // Determine which fields need to be filled and flag the appropriate error. 47 | if (!usernameField) setUsernameError(true); 48 | if (!passwordField) setPasswordError(true); 49 | } 50 | } 51 | 52 | return ( 53 |
54 |
55 |
56 |
57 | {usernameErrorBox} 58 | {signupErrorBox} 59 | setUsernameField(e.target.value)} 65 | > 66 | 67 |
68 |
69 | {passwordErrorBox} 70 | setPasswordField(e.target.value)} 77 | > 78 | 79 |
80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 | ) 88 | } 89 | 90 | export default SignupBox; -------------------------------------------------------------------------------- /client/dashboardComponents/AlertBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AlertBox = () => { 4 | 5 | return ( 6 | 7 | 8 | Alerts 9 | 10 | ); 11 | }; 12 | 13 | export default AlertBox; -------------------------------------------------------------------------------- /client/dashboardComponents/ClusterHealthHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ClusterHealthHeader = () => { 4 | 5 | return ( 6 |
7 |
8 |

Current Health

9 |
10 |
11 | ); 12 | }; 13 | 14 | export default ClusterHealthHeader; -------------------------------------------------------------------------------- /client/dashboardComponents/ComparisonTab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ComparisonTab = () => { 4 | 5 | return ( 6 | 7 | 8 | Comparison 9 | 10 | ); 11 | }; 12 | 13 | export default ComparisonTab; -------------------------------------------------------------------------------- /client/dashboardComponents/Connection.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | const ConnectionModal = ({ modalVisible, closeModal }) => { 4 | if (!modalVisible) return null; 5 | 6 | // State to manage the text field value in the modal 7 | const [promURL, setPromURL] = useState(''); 8 | 9 | // On "Connect" button click, make a fetch request to add new endpoint to current user account 10 | const handlePromURL = () => { 11 | fetch('api/prom/endpoint', { 12 | method: 'POST', 13 | body: JSON.stringify({promURL: promURL}), 14 | headers: {'Content-Type': 'application/json'} 15 | }) 16 | .then(response => { 17 | if (!response.ok) throw new Error('ERROR: Fetch in ConnectionModal\'s handlePromURL failed'); 18 | }) 19 | // Stop rendering this modal after the request is complete 20 | closeModal(); 21 | } 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 |
29 |
30 | Prometheus Server URL: 31 | setPromURL(e.target.value)} 35 | > 36 |
37 |
38 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default ConnectionModal; -------------------------------------------------------------------------------- /client/dashboardComponents/CurrentHealthTab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CurrentHealthTab = () => { 4 | 5 | return ( 6 | 7 | 8 | Current Health 9 | 10 | ); 11 | }; 12 | 13 | export default CurrentHealthTab; -------------------------------------------------------------------------------- /client/dashboardComponents/DashboardPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Sidebar from './Sidebar.jsx'; 3 | import ClusterHealthHeader from './ClusterHealthHeader.jsx'; 4 | import VisualizerBox from './VisualizerBox.jsx'; 5 | import PageMode from './PageMode.jsx'; 6 | 7 | const DashboardPage = ({ logOut }) => { 8 | // declare initial state of our webpage and assign to 'currentHealth' using useState hook 9 | // This can be further expanded upon when more tabs are added 10 | const [tabState, setTabState] = useState('currentHealth'); 11 | 12 | // On clicking the logout button, sends a request to remove jwt cookie 13 | const handleLogOut = () => { 14 | fetch('api/auth/logout', {}) 15 | .then((response) => { 16 | // On a successfull logout, rerender LoginPage 17 | if (response.ok) logOut(); 18 | else throw new Error('ERROR: request failed in handleLogOut'); 19 | }) 20 | .catch((err) => console.log(err)); 21 | }; 22 | 23 | // Commented out PageMode render is for a Dark/Light mode option not yet fully implemented 24 | return ( 25 |
26 |
27 | Fylakas 28 | handleLogOut()} className="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6 text-end" href="#"> 29 | 30 | Log Out 31 | 32 |
33 |
34 |
35 | {} 36 |
37 | {} 38 | {} 39 |
40 |
41 |
42 | {/*
43 | {} 44 |
*/} 45 |
46 | ); 47 | }; 48 | 49 | export default DashboardPage; 50 | -------------------------------------------------------------------------------- /client/dashboardComponents/ErrorBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorBox = () => { 4 | 5 | return ( 6 | 7 | 8 | Errors 9 | 10 | ); 11 | }; 12 | 13 | export default ErrorBox; -------------------------------------------------------------------------------- /client/dashboardComponents/LoadingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Very simple loading page to render before initial Login check is complete 4 | const LoadingPage = () => { 5 | return

Loading...

; 6 | }; 7 | 8 | export default LoadingPage; 9 | -------------------------------------------------------------------------------- /client/dashboardComponents/MetricModal.jsx: -------------------------------------------------------------------------------- 1 | // Prototype of modal for selecting custom metrics 2 | // group by will pop up, as well as more parameters will next to context if a certain metric type is selected. 3 | return ( 4 |
5 |
6 |
7 |
8 |
9 |
10 | Metric Name: 11 | 12 |
13 |
14 | Metric Type: 15 | 26 |
27 |
28 | Context: 29 | 40 |
41 |
42 | Group By: 43 | 54 |
55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 | ); -------------------------------------------------------------------------------- /client/dashboardComponents/PageMode.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | const PageMode = () => { 4 | //Initialize theme state 5 | // const [theme, setTheme] = useState('light'); 6 | // const toggleTheme = (newTheme) => { 7 | // document.documentElement.setAttribute('data-theme', newTheme); // Set the data-theme attribute on the HTML element 8 | // setTheme(newTheme); // Update state 9 | // }; 10 | return ( 11 |
12 | 21 |
    22 |
  • 23 | {/* 33 |
  • 34 |
  • 35 | {/* 45 |
  • 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default PageMode; -------------------------------------------------------------------------------- /client/dashboardComponents/PastHealthTab.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PastHealthTab = () => { 4 | 5 | return ( 6 | 7 | 8 | Past Health 9 | 10 | ); 11 | }; 12 | 13 | export default PastHealthTab; -------------------------------------------------------------------------------- /client/dashboardComponents/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CurrentHealthTab from './CurrentHealthTab.jsx'; 3 | import ComparisonTab from './ComparisonTab.jsx'; 4 | import PastHealthTab from './PastHealthTab.jsx'; 5 | import AlertBox from './AlertBox.jsx'; 6 | import ErrorBox from './ErrorBox.jsx'; 7 | import StatBox from './StatBox.jsx'; 8 | 9 | export default function Sidebar() { 10 | return ( 11 | 35 | ) 36 | } -------------------------------------------------------------------------------- /client/dashboardComponents/StatBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContainerRunningBox from './statBoxComponents/ContainerRunningBox.jsx'; 3 | import NodesRunningBox from './statBoxComponents/NodesRunningBox.jsx'; 4 | import PodsRunningBox from './statBoxComponents/PodsRunningBox.jsx'; 5 | 6 | const StatBox = () => { 7 | 8 | return ( 9 |
10 |
11 |
12 |
13 |

Stats

14 |
15 |
16 |
    17 |
  • Pods Running:
  • 18 |
  • Nodes Running:
  • 19 |
  • Containers Running:
  • 20 |
21 |
22 |
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default StatBox; 29 | -------------------------------------------------------------------------------- /client/dashboardComponents/VisualizerBox.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import VisualizationItem from './visualizerComponents/VisualizationItem.jsx'; 3 | import ConnectionModal from './Connection.jsx'; 4 | 5 | // Number of points of data to display per graph 6 | const range = 60; 7 | // Time to wait in between requests, in milliseconds 8 | const delay = 15000; 9 | // Use dummy data instead of requesting prometheus to test charts 10 | const dummyData = false; 11 | 12 | // Generates a readable HH:MM:SS string from a Date object (i.e., 14:20:54) 13 | const zeroedDate = (date = new Date()) => { 14 | const timePieces = [date.getHours(), date.getMinutes(), date.getSeconds()]; 15 | const zeroedTimePieces = timePieces.map((timePiece) => { 16 | return timePiece < 10 ? '0' + timePiece.toString() : timePiece.toString(); 17 | }); 18 | return `${zeroedTimePieces[0]}:${zeroedTimePieces[1]}:${zeroedTimePieces[2]}`; 19 | }; 20 | 21 | // Removes the appropriate amount of time for first timestamp, in reference to the timerange 22 | function subtractMinutes(date) { 23 | date.setMinutes(date.getMinutes() - range * delay / 60000); 24 | return date; 25 | } 26 | 27 | // Function for generating dummy data (it does math don't worry about it) 28 | const convincingRandomDeviation = (num) => { 29 | return ((num / 100) * 3 + 0.1 ** Math.random()) * 25; 30 | }; 31 | 32 | const VisualizerBox = ({ cluster }) => { 33 | 34 | // Declare an initialDate as the current time and subtracting appropriate minutes from it 35 | const initialDate = zeroedDate(subtractMinutes(new Date()));; 36 | 37 | // Initialize data as an array fitting the range of points 38 | // Fill the array with blank (zeroed) data points 39 | const [liveData, setLiveData] = useState( 40 | Array(range + 1).fill({ date: initialDate, cpu: 0, mem: 0, net: 0, disk: 0 }, 0, range + 1) 41 | ); 42 | 43 | // Declare state for rendering the connection modal 44 | const [modalVisible, setModalVisible] = useState(false); 45 | 46 | // Handle opening/closing the modal 47 | const handleConnectClick = () => { 48 | setModalVisible(true); 49 | }; 50 | 51 | const handleCloseModal = () => { 52 | setModalVisible(false); 53 | }; 54 | 55 | // Requests updates for all datapoints 56 | useEffect(() => { 57 | setTimeout(() => { 58 | // Create a copy of the existing liveData 59 | const newData = liveData.slice(); 60 | // If generating dummy data, push convincing random deviations of the previous datapoints to newData 61 | if (dummyData) { 62 | newData.push({ 63 | date: zeroedDate(), 64 | cpu: convincingRandomDeviation(liveData[range].cpu), 65 | mem: convincingRandomDeviation(liveData[range].mem), 66 | net: convincingRandomDeviation(liveData[range].net), 67 | disk: convincingRandomDeviation(liveData[range].disk), 68 | }); 69 | // If not, send request to prometheus for metrics 70 | } else { 71 | fetch('api/prom/metrics', { 72 | method: 'POST', 73 | body: JSON.stringify({ cluster: cluster }), 74 | headers: { 'Content-Type': 'application/json' }, 75 | }) 76 | .then((response) => { 77 | if (response.ok) return response.json(); 78 | // If request fails, imitate all previous datapoints 79 | return { 80 | date: zeroedDate(), 81 | cpu: liveData[range].cpu, 82 | mem: liveData[range].mem, 83 | net: liveData[range].net, 84 | disk: liveData[range].disk, 85 | } 86 | }) 87 | .then((response) => { 88 | // If any values are missing from request, duplicate previous datapoint for that metric 89 | if (!response.cpu) response.cpu = liveData[range].cpu; 90 | if (!response.mem) response.mem = liveData[range].mem; 91 | if (!response.net) response.net = liveData[range].net; 92 | if (!response.disk) response.disk = liveData[range].disk; 93 | response.date = zeroedDate(); 94 | // Push acquired data to the end of the newData array 95 | newData.push(response); 96 | }) 97 | .catch(err => console.log(err)); 98 | } 99 | // Trim the array down to the appropriate length, removing old data 100 | while (newData.length > range + 1) newData.shift(); 101 | // Update the liveData object with the newData 102 | setLiveData(newData); 103 | }, delay); 104 | // This functionality loops every (delay) milliseconds 105 | // The following dependency ensures the recalling of this useEffect hook 106 | }, [liveData]); 107 | 108 | // Map the dates from liveData to their own array 109 | const dates = liveData.map((datapoint) => datapoint.date); 110 | 111 | // Pass down the relevant metric to each visualization item 112 | // Pass down dates to every visualization item 113 | // Pass down name/coloration of each chart 114 | return ( 115 |
116 |
117 |
118 |

Display Options

119 |
120 | 121 |
122 |
123 |
124 |
125 | { 126 | datapoint.cpu)} 130 | color={'rgb(127, 191, 255)'} 131 | /> 132 | } 133 | { 134 | datapoint.mem)} 138 | color={'rgb(127, 159, 255)'} 139 | /> 140 | } 141 | {/* { 142 | datapoint.net)} 146 | /> 147 | } */} 148 | { 149 | datapoint.disk)} 153 | color={'rgb(127, 127, 255)'} 154 | /> 155 | } 156 |
157 |
158 |
159 | 160 |
161 |
162 | ); 163 | }; 164 | 165 | export default VisualizerBox; 166 | -------------------------------------------------------------------------------- /client/dashboardComponents/statBoxComponents/ContainerRunningBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ContainerRunningBox = () => { 4 | 5 | return ( 6 | //
7 | //

ContainerRunningBox

8 | //
9 |
10 |
11 |
12 |

# of containers running

13 |
14 |
15 |
    16 |
  • Data
  • 17 |
  • Data
  • 18 |
  • Data
  • 19 |
  • Data
  • 20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default ContainerRunningBox; -------------------------------------------------------------------------------- /client/dashboardComponents/statBoxComponents/NodesRunningBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NodesRunningBox = () => { 4 | 5 | return ( 6 | //
7 | //

NodesRunningBox

8 | //
9 |
10 |
11 |
12 |

# of nodes running

13 |
14 |
15 |
    16 |
  • Data
  • 17 |
  • Data
  • 18 |
  • Data
  • 19 |
  • Data
  • 20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default NodesRunningBox; -------------------------------------------------------------------------------- /client/dashboardComponents/statBoxComponents/PodsRunningBox.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PodsRunningBox = () => { 4 | 5 | return ( 6 | //
7 | //

PodsRunningBox

8 | //
9 |
10 |
11 |
12 |

# of pods running

13 |
14 |
15 |
    16 |
  • Data
  • 17 |
  • Data
  • 18 |
  • Data
  • 19 |
  • Data
  • 20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default PodsRunningBox; -------------------------------------------------------------------------------- /client/dashboardComponents/visualizerComponents/VisualizationItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | PointElement, 7 | LineElement, 8 | Title, 9 | Tooltip, 10 | Legend, 11 | } from 'chart.js'; 12 | import { Line } from 'react-chartjs-2'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | PointElement, 18 | LineElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | const VisualizationItem = ({dates, points, name, color}) => { 25 | 26 | // Options object expected by react-chartjs 27 | // The chartjs docs are wonderful (https://www.chartjs.org/docs/latest/) 28 | const options = { 29 | maintainAspectRatio: true, 30 | // Disable animations 31 | animation: false, 32 | // Keep the line optimally curvy 33 | tension: 0.5, 34 | // Remove hover points 35 | pointRadius: 0, 36 | // Declare data ranges and names for X and Y axes 37 | scales: { 38 | x: { 39 | min: 0, 40 | max: 100, 41 | title: { 42 | display: true, 43 | text: 'Time' 44 | }, 45 | ticks: { 46 | maxRotation: 45, 47 | minRotation: 45 48 | } 49 | }, 50 | y: { 51 | min: 0, 52 | max: 100, 53 | title: { 54 | display: true, 55 | text: '% Usage' 56 | } 57 | } 58 | } 59 | }; 60 | 61 | // Declare labels array to insert into data object 62 | const labels = [dates[0]]; 63 | // Fill entirely with blank entries except for the first/last datapoint 64 | for (let i = 2; i < dates.length; i++) labels.push(''); 65 | labels.push(dates[dates.length - 1]); 66 | 67 | // Data object expected by reach-chartjs 68 | const data = { 69 | labels: labels, 70 | datasets: [ 71 | { 72 | borderColor: color, 73 | backgroundColor: color, 74 | label: `Current ${name}`, 75 | // Map the data from properties to the chart 76 | data: points.map((datapoint, index) => { 77 | return {x: index, y: datapoint}; 78 | }) 79 | } 80 | ] 81 | }; 82 | 83 | return ( 84 |
85 |
86 |
87 |

{name}

88 |
89 |
90 | 91 |
92 |
93 |
94 | ); 95 | }; 96 | 97 | export default VisualizationItem; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Fylakas 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fylakas", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack", 8 | "start": "NODE_ENV=production node server/server.js", 9 | "dev": "concurrently \"nodemon server/server.js\" \"NODE_ENV=development webpack serve --open\" ", 10 | "test": "jest" 11 | }, 12 | "jest": { 13 | "testEnvironment": "node", 14 | "testMatch": [ 15 | "**/__tests__/**/?(*.)+(spec|test).[tj]s?(x)" 16 | ] 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@babel/core": "^7.22.15", 22 | "@babel/preset-env": "^7.22.15", 23 | "@babel/preset-react": "^7.22.15", 24 | "babel-core": "^6.26.3", 25 | "babel-jest": "^29.6.4", 26 | "babel-loader": "^9.1.3", 27 | "concurrently": "^8.2.1", 28 | "css-loader": "^6.8.1", 29 | "eslint": "^8.48.0", 30 | "html-webpack-plugin": "^5.5.3", 31 | "jest": "^29.7.0", 32 | "nodemon": "^3.0.1", 33 | "sass-loader": "^13.3.2", 34 | "style-loader": "^3.3.3", 35 | "supertest": "^6.3.3", 36 | "webpack": "^5.88.2", 37 | "webpack-cli": "^5.1.4", 38 | "webpack-dev-server": "^4.15.1" 39 | }, 40 | "dependencies": { 41 | "@kubernetes/client-node": "^0.18.1", 42 | "bcrypt": "^5.1.1", 43 | "bootstrap": "^5.3.1", 44 | "bootstrap-icons": "^1.10.5", 45 | "chart.js": "^4.4.0", 46 | "cookie-parser": "^1.4.6", 47 | "cors": "^2.8.5", 48 | "cross-env": "^7.0.3", 49 | "express": "^4.18.2", 50 | "graphql": "^16.8.0", 51 | "jsonwebtoken": "^9.0.2", 52 | "kubectl": "^0.0.0", 53 | "kubernetes-client": "^9.0.0", 54 | "mongoose": "^7.5.1", 55 | "pg": "^8.11.3", 56 | "postgraphile": "^4.13.0", 57 | "react": "^18.2.0", 58 | "react-chartjs-2": "^5.2.0", 59 | "react-dom": "^18.2.0", 60 | "react-router": "^6.15.0", 61 | "react-router-dom": "^6.15.0", 62 | "sass": "^1.66.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /postgres_create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.users ( 2 | "user_id" serial NOT NULL, 3 | "username" text NOT NULL, 4 | "password" text NOT NULL, 5 | "hash_id" text, 6 | CONSTRAINT "user_pk" PRIMARY KEY ("user_id") 7 | ) WITH ( 8 | OIDS=FALSE 9 | ); 10 | 11 | CREATE TABLE public.clusters ( 12 | "cluster_id" serial NOT NULL, 13 | "user_id" serial NOT NULL REFERENCES public.users(user_id), 14 | "prom_url" text NOT NULL, 15 | CONSTRAINT "clusters_pk" PRIMARY KEY ("cluster_id") 16 | ) WITH ( 17 | OIDS=FALSE 18 | ); 19 | 20 | ALTER TABLE public.clusters ADD CONSTRAINT "users_fk0" FOREIGN KEY ("user_id") REFERENCES public.users("user_id"); -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const jwt = require('jsonwebtoken'); 3 | const db = require('../db/database.js'); 4 | 5 | // Settings for hashing 6 | const WORKFACTOR = 12; 7 | const AUTHKEY = 'd433288a-649e-4f57-8786-4824bf35c5e3'; 8 | 9 | const authController = {}; 10 | 11 | // Initializes any login/signup request 12 | authController.handleUserDetails = async (req, res, next) => { 13 | try { 14 | res.locals.valid = true; 15 | const { username, password } = req.body; 16 | console.log(`Handling auth request for "${username}".`); 17 | // Input validation for username/password 18 | if (!username || typeof username !== 'string') { 19 | res.locals.valid = false; 20 | console.log(`Invalid username format in signup.`); 21 | } 22 | if (!password || typeof password !== 'string') { 23 | res.locals.valid = false; 24 | console.log('Invalid password format in signup.'); 25 | } 26 | // Store a hashed version of the input password 27 | if (res.locals.valid) 28 | res.locals.hpassword = await bcrypt.hash(password, WORKFACTOR); 29 | next(); 30 | } catch (err) { 31 | next({ 32 | log: err, 33 | message: { err: 'Error occurred handling user details.' }, 34 | }); 35 | } 36 | }; 37 | 38 | // Handles a login request 39 | authController.login = async (req, res, next) => { 40 | try { 41 | const { username, password } = req.body; 42 | const { valid } = res.locals; 43 | // Only procede if previous steps returned valid; otherwise, skip this step. 44 | if (valid) { 45 | console.log(`Making login request with username "${username}".`); 46 | // Select user details matching the username from the request 47 | const user = await db.query( 48 | 'SELECT username, hash_id, password FROM public.users WHERE username=$1;', 49 | [username] 50 | ); 51 | if ( 52 | // Check to see if there's a returned a user with the username 53 | user.rows[0] && 54 | // Check to see if the password for that user is correct 55 | (await bcrypt.compare(password, user.rows[0].password)) 56 | ) { 57 | // If details are correct, store the hashed user ID 58 | console.log(`Succesfully verified login details for "${username}".`); 59 | res.locals.id = user.rows[0].hash_id; 60 | } else { 61 | // If details are incorrect, reject the attempt 62 | console.log('Username or password is incorrect.'); 63 | res.locals.valid = false; 64 | } 65 | } 66 | next(); 67 | } catch (err) { 68 | next({ log: err, message: { err: 'Error occurred in login.' } }); 69 | } 70 | }; 71 | 72 | // Handles a signup request 73 | authController.signup = async (req, res, next) => { 74 | try { 75 | const { username } = req.body; 76 | const { valid, hpassword } = res.locals; 77 | // Only procede if previous steps returned valid; otherwise, skip this step. 78 | if (valid) { 79 | console.log(`Making signup request with username "${username}".`); 80 | // Select user details matching the username from the request 81 | const matchedUsernames = await db.query( 82 | 'SELECT * FROM public.users WHERE username = $1;', 83 | [username] 84 | ); 85 | if (matchedUsernames.rows[0]) { 86 | // If username already exists, reject the signup attempt 87 | console.log(`Username "${username}" already exists.`); 88 | res.locals.valid = false; 89 | } else { 90 | // On valid signup attempt, add new user to user table with username and hashed password 91 | // Return and store the id associated with this user 92 | console.log(`Creating new account in database for "${username}".`); 93 | const insertedUser = await db.query( 94 | 'INSERT INTO public.users (username, password) VALUES ($1, $2) RETURNING user_id;', 95 | [username, hpassword] 96 | ); 97 | // Store a hashed version of the userID on the user profile 98 | const newUserId = insertedUser.rows[0].user_id.toString(); 99 | const hashID = await bcrypt.hash(newUserId, WORKFACTOR); 100 | await db.query( 101 | `UPDATE public.users SET hash_id = '${hashID}' WHERE user_id = '${newUserId}'` 102 | ); 103 | res.locals.id = hashID; 104 | } 105 | } 106 | next(); 107 | } catch (err) { 108 | next({ log: err, message: { err: 'Error occurred in signup.' } }); 109 | } 110 | }; 111 | 112 | // Creates new user session on successful login/signup 113 | authController.startSession = async (req, res, next) => { 114 | try { 115 | const { username } = req.body; 116 | const { valid, id } = res.locals; 117 | // Only procede if previous steps returned valid; otherwise, skip this step. 118 | if (valid) { 119 | console.log(`Starting session for username "${username}".`); 120 | // Create a jwt from the username that expires in 1 day 121 | const token = jwt.sign({ username: req.body.username }, AUTHKEY, { 122 | expiresIn: '86400s', 123 | }); 124 | // Store the jwt as a cookie 125 | res.cookie('jwt', token, { 126 | maxAge: 86400000, 127 | httpOnly: true, 128 | }); 129 | // Store hashed ID onto profile 130 | res.locals.profile = { id: id }; 131 | } else { 132 | console.log(`Refusing to start session for ${username}.`); 133 | // If skipped session start attempt, assign profile as null 134 | res.locals.profile = null; 135 | } 136 | next(); 137 | } catch (err) { 138 | next({ 139 | log: err, 140 | message: { err: 'Error occurred starting user session.' }, 141 | }); 142 | } 143 | }; 144 | 145 | // Check to see what user is logged in, if any, on a request 146 | authController.isLoggedIn = async (req, res, next) => { 147 | try { 148 | console.log(`Checking cookies.`); 149 | res.locals.isLoggedIn = true; 150 | // Verify the jwt from the request, if it exists 151 | let payload; 152 | try { 153 | payload = await jwt.verify(req.cookies.jwt, AUTHKEY); 154 | } catch (err) { 155 | // Declare username on payload as null on failed jwt verification 156 | payload = { username: null }; 157 | console.log('Rejected cookie.'); 158 | } 159 | res.locals.username = payload.username; 160 | // Find user account associated with username from cookie 161 | const matchedAccounts = await db.query( 162 | 'SELECT username FROM public.users WHERE username = $1;', 163 | [payload.username] 164 | ); 165 | if (!matchedAccounts.rows[0]) { 166 | // If username doesn't exist, reject attempt 167 | res.locals.isLoggedIn = false; 168 | } else { 169 | // If username exists, store username for future middleware 170 | res.locals.username = matchedAccounts.rows[0].username; 171 | console.log(`Verified cookie for ${res.locals.username}.`) 172 | } 173 | next(); 174 | } catch (err) { 175 | next({ 176 | log: err, 177 | message: { err: 'Error occurred checking user credentials.' }, 178 | }); 179 | } 180 | }; 181 | 182 | // Log a user out 183 | authController.logout = async (req, res, next) => { 184 | try { 185 | // Removes any jwt cookie from request 186 | res.clearCookie('jwt'); 187 | return next(); 188 | } catch (err) { 189 | next({ log: err, message: { err: 'Error occurred while logging out.' } }); 190 | } 191 | }; 192 | 193 | module.exports = authController; 194 | -------------------------------------------------------------------------------- /server/controllers/k8sController.js: -------------------------------------------------------------------------------- 1 | // NOT IMPLEMENTED IN CURRENT VERSION. 2 | 3 | const express = require('express'); 4 | // import * as k8s from '@kubernetes/client-node'; 5 | const k8s = require('@kubernetes/client-node'); 6 | 7 | 8 | // connect config from kat root directory 9 | const kc = new k8s.KubeConfig(); 10 | kc.loadFromFile('/Users/katfry/.kube/config'); 11 | 12 | 13 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 14 | if (k8sApi) console.log('user is connected'); 15 | else console.log('problem connecting'); 16 | const k8sApi2 = kc.makeApiClient(k8s.AppsV1Api); 17 | 18 | // Initialize an empty object to hold Kubernetes controller functions 19 | const k8sController = {}; 20 | 21 | // Define a function to get information about Kubernetes nodes 22 | k8sController.getNodes = async (req, res, next 23 | ) => { 24 | // console.log('K8s API:', k8sApi); 25 | try { 26 | // Fetch node information from the Kubernetes API 27 | const data = await k8sApi.listNode(); 28 | // console.log('Data returned:', data); 29 | // Process the fetched data and map it into an array of Node objects 30 | const nodes = data.body.items.map(data => { 31 | const { name, namespace, creationTimestamp, labels, uid } = data.metadata; 32 | const { providerID } = data.spec; 33 | const { status } = data; 34 | const node = { 35 | name, 36 | namespace, 37 | creationTimestamp, 38 | uid, 39 | labels, 40 | providerID, 41 | status, 42 | }; 43 | // console.log('this is the node info', node); 44 | return node; 45 | }); 46 | 47 | // Store the processed Nodes in the response locals object 48 | res.locals.nodes = nodes; 49 | 50 | // Store cluster information in the response locals 51 | res.locals.cluster = { nodes: nodes }; 52 | 53 | // Call the 'next' function to continue along the middleware chain 54 | return next(); 55 | } catch (error) { 56 | // Handle errors by sending an error response 57 | return next({ 58 | log: 'Error caught in k8sController getNodes', 59 | error, 60 | status: 400, 61 | message: { 62 | err: `An error occured when fetching Node information from the Kubernetes API. Error: ${error.message}`, 63 | }, 64 | }); 65 | } 66 | }; 67 | 68 | // Define a function to get information about the Kubernetes pods 69 | k8sController.getPods = async (req, res, next 70 | ) => { 71 | try { 72 | // Fetch pod data from the Kubernetes API 73 | const data = await k8sApi.listPodForAllNamespaces(); 74 | // console.log('pod namespace items', data.body.items); 75 | 76 | // Process the fetched data and map it to an array of Pod objects 77 | const pods = data.body.items.map(data => { 78 | const { name, namespace, creationTimestamp, uid, labels } = data.metadata; 79 | const { nodeName, containers, serviceAccount } = data.spec; 80 | const { conditions, containerStatuses, phase, podIP } = data.status; 81 | const pod= { 82 | name, 83 | namespace, 84 | uid, 85 | creationTimestamp, 86 | labels, 87 | nodeName, 88 | containers, 89 | serviceAccount, 90 | conditions, 91 | containerStatuses, 92 | phase, 93 | podIP, 94 | }; 95 | return pod; 96 | }); 97 | 98 | // Store the processed data in the res.locals 99 | res.locals.pods = pods; 100 | 101 | // Store the processed data in the cluster property of res.locals 102 | res.locals.cluster = { pods: pods }; 103 | // Call the 'next' function to continue along the middleware chain 104 | next(); 105 | } catch (error) { 106 | // Handle errors by sending an error response 107 | return next({ 108 | log: 'Error caught in k8sController getPods', 109 | error, 110 | status: 400, 111 | message: { 112 | err: 'An error occured when fetching Pod information from the Kubernetes API', 113 | }, 114 | }); 115 | } 116 | }; 117 | 118 | k8sController.getNamespaces = async (req, res, next 119 | ) => { 120 | try { 121 | const data = await k8sApi.listNamespace(); 122 | const namespaces = data.body.items.map(data => { 123 | const { name, creationTimestamp, labels, uid } = data.metadata; 124 | const { phase } = data.status; 125 | const nodeName = ''; 126 | const namespace= { 127 | name, 128 | uid, 129 | creationTimestamp, 130 | labels, 131 | phase, 132 | nodeName, 133 | }; 134 | return namespace; 135 | }); 136 | res.locals.namespaces = namespaces; 137 | res.locals.cluster = { namespaces: namespaces }; 138 | next(); 139 | } catch (error) { 140 | // Handle errors by sending an error response 141 | return next({ 142 | log: 'Error caught in k8sController getNamespaces', 143 | error, 144 | status: 400, 145 | message: { 146 | err: 'An error occured when fetching Namespace information from the Kubernetes API', 147 | }, 148 | }); 149 | } 150 | }; 151 | 152 | k8sController.getServices = async (req, res, next 153 | ) => { 154 | try { 155 | const data = await k8sApi.listServiceForAllNamespaces(); 156 | const services = data.body.items.map(data => { 157 | const { name, namespace, uid, creationTimestamp, labels } = data.metadata; 158 | const { ports, clusterIP } = data.spec; 159 | const { loadBalancer } = data.status; 160 | const service = { 161 | name, 162 | namespace, 163 | uid, 164 | creationTimestamp, 165 | labels, 166 | ports, 167 | loadBalancer, 168 | clusterIP, 169 | }; 170 | return service; 171 | }); 172 | res.locals.services = services; 173 | res.locals.cluster = { services: services }; 174 | next(); 175 | } catch (error) { 176 | // Handle errors by sending an error response 177 | return next({ 178 | log: 'Error caught in k8sController getServices', 179 | error, 180 | status: 400, 181 | message: { 182 | err: 'An error occured when fetching Service information from the Kubernetes API', 183 | }, 184 | }); 185 | } 186 | }; 187 | 188 | k8sController.getDeployments = async (req, res, next 189 | ) => { 190 | try { 191 | const data = await k8sApi2.listDeploymentForAllNamespaces(); 192 | // console.log('deployment data', data); 193 | 194 | const deployments = data.body.items.map(data => { 195 | const { name, creationTimestamp, labels, namespace, uid } = data.metadata; 196 | const { replicas } = data.spec; 197 | const { status } = data.status; 198 | const deployment = { 199 | name, 200 | namespace, 201 | uid, 202 | creationTimestamp, 203 | labels, 204 | replicas, 205 | status, 206 | }; 207 | return deployment; 208 | }); 209 | res.locals.deployments = deployments; 210 | res.locals.cluster = { deployments: deployments }; 211 | next(); 212 | } catch (error) { 213 | // Handle errors by sending an error response 214 | return next({ 215 | log: 'Error caught in k8sController getDeployments', 216 | error, 217 | status: 400, 218 | message: { 219 | err: 'An error occured when fetching Deployment information from the Kubernetes API', 220 | }, 221 | }); 222 | } 223 | }; 224 | 225 | // Define a function to get information about the cluster 226 | k8sController.getCluster = async (req, res, next 227 | ) => { 228 | try { 229 | // Retrieve data about nodes, pods, namespaces, services, and deployments from res.locals 230 | const nodes = res.locals.nodes; 231 | const pods = res.locals.pods; 232 | const namespaces = res.locals.namespaces; 233 | const services = res.locals.services; 234 | const deployments = res.locals.deployments; 235 | 236 | // Create a Cluster object that contains information about the entire cluster 237 | const cluster = { 238 | nodes, 239 | pods, 240 | namespaces, 241 | services, 242 | deployments, 243 | }; 244 | 245 | // Store the cluster information in the response locals 246 | //currently does not accounting for multiple clusters, just one cluster for now 247 | res.locals.cluster = cluster; 248 | // Call the 'next' function to continue along the middleware chain 249 | next(); 250 | } catch (error) { 251 | // Handle errors by sending an error response 252 | return next({ 253 | log: 'Error caught in k8sController getCluster', 254 | error, 255 | status: 400, 256 | message: { 257 | err: 'An error occured when fetching Cluster information from the Kubernetes API', 258 | }, 259 | }); 260 | } 261 | }; 262 | 263 | // this middleware is for node usage (CPU, memory, pods, images) 264 | k8sController.nodeStatus = async(req, res, next) => { 265 | try { 266 | // Fetch node information from the Kubernetes API 267 | const data = await k8sApi.listNode(); 268 | // map data to find the status properties (allocatable, capacity, images) with cpu and memory for each 269 | const nodeUsageData = data.body.items.map(data => { 270 | const { creationTimestamp } = data.metadata; 271 | // console.log('creationTimestamp', creationTimestamp); 272 | const { allocatable, capacity, images } = data.status; 273 | //const timeStamp = creationTimestamp; 274 | const usage = { 275 | creationTimestamp, 276 | allocatable, 277 | capacity, 278 | images 279 | }; 280 | // console.log('this is usage', usage); 281 | return usage; 282 | }); 283 | res.locals.nodeUsage = nodeUsageData; 284 | console.log('this is nodeUsageData in nodeStatus controller', nodeUsageData); 285 | return next(); 286 | } catch(err) { 287 | return next({ 288 | log: 'Error caught in k8sController nodeStatus middleware', 289 | err, 290 | status: 400, 291 | message: { 292 | err: `An error occurred when fetching node status info from the k8sApi. Error: ${err.message}` 293 | } 294 | }); 295 | } 296 | } 297 | 298 | // this middleware is for pod usage (CPU, memory, container stats) 299 | k8sController.podStatus = async (req, res, next) => { 300 | console.log('in the podStatus middleware'); 301 | //console.log(k8sApi); 302 | try { 303 | // Fetch pod data from the Kubernetes API 304 | const data = res.locals.cluster; 305 | // console.log('data in podStatus', data); 306 | const podUsageData = data.pods.map(data => { 307 | // destructure data to include metrics we want (timestamp from metadata) 308 | const { namespace, creationTimestamp, containers, phase } = data; 309 | // declare object called usage assigned to metrics we're grabbing 310 | const podUsage = { 311 | namespace, 312 | creationTimestamp, 313 | containers, 314 | phase, 315 | }; 316 | // return usage 317 | return podUsage; 318 | }); 319 | // assign res.locals.podUsage to podUsageData 320 | res.locals.podUsage = podUsageData; 321 | // console.log('this is podUsageData', podUsageData); 322 | return next(); 323 | } catch (err) { 324 | return next({ 325 | log: 'Error caught in k8sController podStatus middleware', 326 | err, 327 | status: 400, 328 | message: { 329 | err: `An error occurred when fetching pod status info from the k8sApi. Error: ${err.message}` 330 | } 331 | }); 332 | } 333 | } 334 | 335 | module.exports = k8sController; 336 | 337 | /* 338 | this is output of GET to localhost:3000/api/k8s/cluster 339 | { 340 | "nodes": [ 341 | { 342 | "name": "minikube", 343 | "creationTimestamp": "2023-09-13T12:16:15.000Z", 344 | "uid": "03b017a9-367f-4826-bd4d-b86d719049b9", 345 | "labels": { 346 | "beta.kubernetes.io/arch": "amd64", 347 | "beta.kubernetes.io/os": "linux", 348 | "kubernetes.io/arch": "amd64", 349 | "kubernetes.io/hostname": "minikube", 350 | "kubernetes.io/os": "linux", 351 | "minikube.k8s.io/commit": "fd7ecd9c4599bef9f04c0986c4a0187f98a4396e", 352 | "minikube.k8s.io/name": "minikube", 353 | "minikube.k8s.io/primary": "true", 354 | "minikube.k8s.io/updated_at": "2023_09_13T08_16_26_0700", 355 | "minikube.k8s.io/version": "v1.31.2", 356 | "node-role.kubernetes.io/control-plane": "", 357 | "node.kubernetes.io/exclude-from-external-load-balancers": "" 358 | }, 359 | "status": { 360 | "addresses": [ 361 | { 362 | "address": "192.168.49.2", 363 | "type": "InternalIP" 364 | }, 365 | { 366 | "address": "minikube", 367 | "type": "Hostname" 368 | } 369 | ], 370 | "allocatable": { 371 | "cpu": "2", 372 | "ephemeral-storage": "61202244Ki", 373 | "hugepages-2Mi": "0", 374 | "memory": "8048516Ki", 375 | "pods": "110" 376 | }, 377 | "capacity": { 378 | "cpu": "2", 379 | "ephemeral-storage": "61202244Ki", 380 | "hugepages-2Mi": "0", 381 | "memory": "8048516Ki", 382 | "pods": "110" 383 | }, 384 | "conditions": [ 385 | { 386 | "lastHeartbeatTime": "2023-09-13T12:32:06.000Z", 387 | "lastTransitionTime": "2023-09-13T12:16:14.000Z", 388 | "message": "kubelet has sufficient memory available", 389 | "reason": "KubeletHasSufficientMemory", 390 | "status": "False", 391 | "type": "MemoryPressure" 392 | }, 393 | { 394 | "lastHeartbeatTime": "2023-09-13T12:32:06.000Z", 395 | "lastTransitionTime": "2023-09-13T12:16:14.000Z", 396 | "message": "kubelet has no disk pressure", 397 | "reason": "KubeletHasNoDiskPressure", 398 | "status": "False", 399 | "type": "DiskPressure" 400 | }, 401 | { 402 | "lastHeartbeatTime": "2023-09-13T12:32:06.000Z", 403 | "lastTransitionTime": "2023-09-13T12:16:14.000Z", 404 | "message": "kubelet has sufficient PID available", 405 | "reason": "KubeletHasSufficientPID", 406 | "status": "False", 407 | "type": "PIDPressure" 408 | }, 409 | { 410 | "lastHeartbeatTime": "2023-09-13T12:32:06.000Z", 411 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 412 | "message": "kubelet is posting ready status", 413 | "reason": "KubeletReady", 414 | "status": "True", 415 | "type": "Ready" 416 | } 417 | ], 418 | "daemonEndpoints": { 419 | "kubeletEndpoint": { 420 | "Port": 10250 421 | } 422 | }, 423 | "images": [ 424 | { 425 | "names": [ 426 | "registry.k8s.io/etcd@sha256:51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83", 427 | "registry.k8s.io/etcd:3.5.7-0" 428 | ], 429 | "sizeBytes": 295724043 430 | }, 431 | { 432 | "names": [ 433 | "registry.k8s.io/kube-apiserver@sha256:697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d", 434 | "registry.k8s.io/kube-apiserver:v1.27.4" 435 | ], 436 | "sizeBytes": 120653626 437 | }, 438 | { 439 | "names": [ 440 | "registry.k8s.io/kube-controller-manager@sha256:6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265", 441 | "registry.k8s.io/kube-controller-manager:v1.27.4" 442 | ], 443 | "sizeBytes": 112507033 444 | }, 445 | { 446 | "names": [ 447 | "registry.k8s.io/kube-proxy@sha256:4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf", 448 | "registry.k8s.io/kube-proxy:v1.27.4" 449 | ], 450 | "sizeBytes": 71122088 451 | }, 452 | { 453 | "names": [ 454 | "registry.k8s.io/kube-scheduler@sha256:5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af", 455 | "registry.k8s.io/kube-scheduler:v1.27.4" 456 | ], 457 | "sizeBytes": 58390668 458 | }, 459 | { 460 | "names": [ 461 | "registry.k8s.io/coredns/coredns@sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e", 462 | "registry.k8s.io/coredns/coredns:v1.10.1" 463 | ], 464 | "sizeBytes": 53612153 465 | }, 466 | { 467 | "names": [ 468 | "gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944", 469 | "gcr.io/k8s-minikube/storage-provisioner:v5" 470 | ], 471 | "sizeBytes": 31465472 472 | }, 473 | { 474 | "names": [ 475 | "registry.k8s.io/pause@sha256:7031c1b283388d2c2e09b57badb803c05ebed362dc88d84b480cc47f72a21097", 476 | "registry.k8s.io/pause:3.9" 477 | ], 478 | "sizeBytes": 743952 479 | } 480 | ], 481 | "nodeInfo": { 482 | "architecture": "amd64", 483 | "bootID": "33bfa64e-4e01-4372-b199-5f4c9940baad", 484 | "containerRuntimeVersion": "docker://24.0.4", 485 | "kernelVersion": "5.15.49-linuxkit-pr", 486 | "kubeProxyVersion": "v1.27.4", 487 | "kubeletVersion": "v1.27.4", 488 | "machineID": "94e2e64d19cc417681d49158d95cadf5", 489 | "operatingSystem": "linux", 490 | "osImage": "Ubuntu 22.04.2 LTS", 491 | "systemUUID": "94e2e64d19cc417681d49158d95cadf5" 492 | } 493 | } 494 | } 495 | ], 496 | "pods": [ 497 | { 498 | "name": "coredns-5d78c9869d-ww9gd", 499 | "namespace": "kube-system", 500 | "uid": "40852688-7d60-426c-bf08-5f63b23fb37e", 501 | "creationTimestamp": "2023-09-13T12:16:42.000Z", 502 | "labels": { 503 | "k8s-app": "kube-dns", 504 | "pod-template-hash": "5d78c9869d" 505 | }, 506 | "nodeName": "minikube", 507 | "containers": [ 508 | { 509 | "args": [ 510 | "-conf", 511 | "/etc/coredns/Corefile" 512 | ], 513 | "image": "registry.k8s.io/coredns/coredns:v1.10.1", 514 | "imagePullPolicy": "IfNotPresent", 515 | "livenessProbe": { 516 | "failureThreshold": 5, 517 | "httpGet": { 518 | "path": "/health", 519 | "port": 8080, 520 | "scheme": "HTTP" 521 | }, 522 | "initialDelaySeconds": 60, 523 | "periodSeconds": 10, 524 | "successThreshold": 1, 525 | "timeoutSeconds": 5 526 | }, 527 | "name": "coredns", 528 | "ports": [ 529 | { 530 | "containerPort": 53, 531 | "name": "dns", 532 | "protocol": "UDP" 533 | }, 534 | { 535 | "containerPort": 53, 536 | "name": "dns-tcp", 537 | "protocol": "TCP" 538 | }, 539 | { 540 | "containerPort": 9153, 541 | "name": "metrics", 542 | "protocol": "TCP" 543 | } 544 | ], 545 | "readinessProbe": { 546 | "failureThreshold": 3, 547 | "httpGet": { 548 | "path": "/ready", 549 | "port": 8181, 550 | "scheme": "HTTP" 551 | }, 552 | "periodSeconds": 10, 553 | "successThreshold": 1, 554 | "timeoutSeconds": 1 555 | }, 556 | "resources": { 557 | "limits": { 558 | "memory": "170Mi" 559 | }, 560 | "requests": { 561 | "cpu": "100m", 562 | "memory": "70Mi" 563 | } 564 | }, 565 | "securityContext": { 566 | "allowPrivilegeEscalation": false, 567 | "capabilities": { 568 | "add": [ 569 | "NET_BIND_SERVICE" 570 | ], 571 | "drop": [ 572 | "all" 573 | ] 574 | }, 575 | "readOnlyRootFilesystem": true 576 | }, 577 | "terminationMessagePath": "/dev/termination-log", 578 | "terminationMessagePolicy": "File", 579 | "volumeMounts": [ 580 | { 581 | "mountPath": "/etc/coredns", 582 | "name": "config-volume", 583 | "readOnly": true 584 | }, 585 | { 586 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 587 | "name": "kube-api-access-jbw95", 588 | "readOnly": true 589 | } 590 | ] 591 | } 592 | ], 593 | "serviceAccount": "coredns", 594 | "conditions": [ 595 | { 596 | "lastProbeTime": null, 597 | "lastTransitionTime": "2023-09-13T12:16:43.000Z", 598 | "status": "True", 599 | "type": "Initialized" 600 | }, 601 | { 602 | "lastProbeTime": null, 603 | "lastTransitionTime": "2023-09-13T12:16:53.000Z", 604 | "status": "True", 605 | "type": "Ready" 606 | }, 607 | { 608 | "lastProbeTime": null, 609 | "lastTransitionTime": "2023-09-13T12:16:53.000Z", 610 | "status": "True", 611 | "type": "ContainersReady" 612 | }, 613 | { 614 | "lastProbeTime": null, 615 | "lastTransitionTime": "2023-09-13T12:16:43.000Z", 616 | "status": "True", 617 | "type": "PodScheduled" 618 | } 619 | ], 620 | "containerStatuses": [ 621 | { 622 | "containerID": "docker://381f9edc0884322b69b428fb2d7d087bc1c932cde96b1b1b18f226a7ead7df79", 623 | "image": "registry.k8s.io/coredns/coredns:v1.10.1", 624 | "imageID": "docker-pullable://registry.k8s.io/coredns/coredns@sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e", 625 | "lastState": {}, 626 | "name": "coredns", 627 | "ready": true, 628 | "restartCount": 0, 629 | "started": true, 630 | "state": { 631 | "running": { 632 | "startedAt": "2023-09-13T12:16:50.000Z" 633 | } 634 | } 635 | } 636 | ], 637 | "phase": "Running", 638 | "podIP": "10.244.0.2" 639 | }, 640 | { 641 | "name": "etcd-minikube", 642 | "namespace": "kube-system", 643 | "uid": "8ef32248-0a4d-4f85-84db-3a0cc40d2bf1", 644 | "creationTimestamp": "2023-09-13T12:16:26.000Z", 645 | "labels": { 646 | "component": "etcd", 647 | "tier": "control-plane" 648 | }, 649 | "nodeName": "minikube", 650 | "containers": [ 651 | { 652 | "command": [ 653 | "etcd", 654 | "--advertise-client-urls=https://192.168.49.2:2379", 655 | "--cert-file=/var/lib/minikube/certs/etcd/server.crt", 656 | "--client-cert-auth=true", 657 | "--data-dir=/var/lib/minikube/etcd", 658 | "--experimental-initial-corrupt-check=true", 659 | "--experimental-watch-progress-notify-interval=5s", 660 | "--initial-advertise-peer-urls=https://192.168.49.2:2380", 661 | "--initial-cluster=minikube=https://192.168.49.2:2380", 662 | "--key-file=/var/lib/minikube/certs/etcd/server.key", 663 | "--listen-client-urls=https://127.0.0.1:2379,https://192.168.49.2:2379", 664 | "--listen-metrics-urls=http://127.0.0.1:2381", 665 | "--listen-peer-urls=https://192.168.49.2:2380", 666 | "--name=minikube", 667 | "--peer-cert-file=/var/lib/minikube/certs/etcd/peer.crt", 668 | "--peer-client-cert-auth=true", 669 | "--peer-key-file=/var/lib/minikube/certs/etcd/peer.key", 670 | "--peer-trusted-ca-file=/var/lib/minikube/certs/etcd/ca.crt", 671 | "--proxy-refresh-interval=70000", 672 | "--snapshot-count=10000", 673 | "--trusted-ca-file=/var/lib/minikube/certs/etcd/ca.crt" 674 | ], 675 | "image": "registry.k8s.io/etcd:3.5.7-0", 676 | "imagePullPolicy": "IfNotPresent", 677 | "livenessProbe": { 678 | "failureThreshold": 8, 679 | "httpGet": { 680 | "host": "127.0.0.1", 681 | "path": "/health?exclude=NOSPACE&serializable=true", 682 | "port": 2381, 683 | "scheme": "HTTP" 684 | }, 685 | "initialDelaySeconds": 10, 686 | "periodSeconds": 10, 687 | "successThreshold": 1, 688 | "timeoutSeconds": 15 689 | }, 690 | "name": "etcd", 691 | "resources": { 692 | "requests": { 693 | "cpu": "100m", 694 | "memory": "100Mi" 695 | } 696 | }, 697 | "startupProbe": { 698 | "failureThreshold": 24, 699 | "httpGet": { 700 | "host": "127.0.0.1", 701 | "path": "/health?serializable=false", 702 | "port": 2381, 703 | "scheme": "HTTP" 704 | }, 705 | "initialDelaySeconds": 10, 706 | "periodSeconds": 10, 707 | "successThreshold": 1, 708 | "timeoutSeconds": 15 709 | }, 710 | "terminationMessagePath": "/dev/termination-log", 711 | "terminationMessagePolicy": "File", 712 | "volumeMounts": [ 713 | { 714 | "mountPath": "/var/lib/minikube/etcd", 715 | "name": "etcd-data" 716 | }, 717 | { 718 | "mountPath": "/var/lib/minikube/certs/etcd", 719 | "name": "etcd-certs" 720 | } 721 | ] 722 | } 723 | ], 724 | "conditions": [ 725 | { 726 | "lastProbeTime": null, 727 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 728 | "status": "True", 729 | "type": "Initialized" 730 | }, 731 | { 732 | "lastProbeTime": null, 733 | "lastTransitionTime": "2023-09-13T12:16:33.000Z", 734 | "status": "True", 735 | "type": "Ready" 736 | }, 737 | { 738 | "lastProbeTime": null, 739 | "lastTransitionTime": "2023-09-13T12:16:33.000Z", 740 | "status": "True", 741 | "type": "ContainersReady" 742 | }, 743 | { 744 | "lastProbeTime": null, 745 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 746 | "status": "True", 747 | "type": "PodScheduled" 748 | } 749 | ], 750 | "containerStatuses": [ 751 | { 752 | "containerID": "docker://c376067b1cb2c836bb7967f4cd8f57cfa9e9583232d7b75775e848251bdba44d", 753 | "image": "registry.k8s.io/etcd:3.5.7-0", 754 | "imageID": "docker-pullable://registry.k8s.io/etcd@sha256:51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83", 755 | "lastState": {}, 756 | "name": "etcd", 757 | "ready": true, 758 | "restartCount": 0, 759 | "started": true, 760 | "state": { 761 | "running": { 762 | "startedAt": "2023-09-13T12:15:51.000Z" 763 | } 764 | } 765 | } 766 | ], 767 | "phase": "Running", 768 | "podIP": "192.168.49.2" 769 | }, 770 | { 771 | "name": "kube-apiserver-minikube", 772 | "namespace": "kube-system", 773 | "uid": "7d77cb36-6f1d-4ad8-8c23-306befaeb1d3", 774 | "creationTimestamp": "2023-09-13T12:16:26.000Z", 775 | "labels": { 776 | "component": "kube-apiserver", 777 | "tier": "control-plane" 778 | }, 779 | "nodeName": "minikube", 780 | "containers": [ 781 | { 782 | "command": [ 783 | "kube-apiserver", 784 | "--advertise-address=192.168.49.2", 785 | "--allow-privileged=true", 786 | "--authorization-mode=Node,RBAC", 787 | "--client-ca-file=/var/lib/minikube/certs/ca.crt", 788 | "--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota", 789 | "--enable-bootstrap-token-auth=true", 790 | "--etcd-cafile=/var/lib/minikube/certs/etcd/ca.crt", 791 | "--etcd-certfile=/var/lib/minikube/certs/apiserver-etcd-client.crt", 792 | "--etcd-keyfile=/var/lib/minikube/certs/apiserver-etcd-client.key", 793 | "--etcd-servers=https://127.0.0.1:2379", 794 | "--kubelet-client-certificate=/var/lib/minikube/certs/apiserver-kubelet-client.crt", 795 | "--kubelet-client-key=/var/lib/minikube/certs/apiserver-kubelet-client.key", 796 | "--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname", 797 | "--proxy-client-cert-file=/var/lib/minikube/certs/front-proxy-client.crt", 798 | "--proxy-client-key-file=/var/lib/minikube/certs/front-proxy-client.key", 799 | "--requestheader-allowed-names=front-proxy-client", 800 | "--requestheader-client-ca-file=/var/lib/minikube/certs/front-proxy-ca.crt", 801 | "--requestheader-extra-headers-prefix=X-Remote-Extra-", 802 | "--requestheader-group-headers=X-Remote-Group", 803 | "--requestheader-username-headers=X-Remote-User", 804 | "--secure-port=8443", 805 | "--service-account-issuer=https://kubernetes.default.svc.cluster.local", 806 | "--service-account-key-file=/var/lib/minikube/certs/sa.pub", 807 | "--service-account-signing-key-file=/var/lib/minikube/certs/sa.key", 808 | "--service-cluster-ip-range=10.96.0.0/12", 809 | "--tls-cert-file=/var/lib/minikube/certs/apiserver.crt", 810 | "--tls-private-key-file=/var/lib/minikube/certs/apiserver.key" 811 | ], 812 | "image": "registry.k8s.io/kube-apiserver:v1.27.4", 813 | "imagePullPolicy": "IfNotPresent", 814 | "livenessProbe": { 815 | "failureThreshold": 8, 816 | "httpGet": { 817 | "host": "192.168.49.2", 818 | "path": "/livez", 819 | "port": 8443, 820 | "scheme": "HTTPS" 821 | }, 822 | "initialDelaySeconds": 10, 823 | "periodSeconds": 10, 824 | "successThreshold": 1, 825 | "timeoutSeconds": 15 826 | }, 827 | "name": "kube-apiserver", 828 | "readinessProbe": { 829 | "failureThreshold": 3, 830 | "httpGet": { 831 | "host": "192.168.49.2", 832 | "path": "/readyz", 833 | "port": 8443, 834 | "scheme": "HTTPS" 835 | }, 836 | "periodSeconds": 1, 837 | "successThreshold": 1, 838 | "timeoutSeconds": 15 839 | }, 840 | "resources": { 841 | "requests": { 842 | "cpu": "250m" 843 | } 844 | }, 845 | "startupProbe": { 846 | "failureThreshold": 24, 847 | "httpGet": { 848 | "host": "192.168.49.2", 849 | "path": "/livez", 850 | "port": 8443, 851 | "scheme": "HTTPS" 852 | }, 853 | "initialDelaySeconds": 10, 854 | "periodSeconds": 10, 855 | "successThreshold": 1, 856 | "timeoutSeconds": 15 857 | }, 858 | "terminationMessagePath": "/dev/termination-log", 859 | "terminationMessagePolicy": "File", 860 | "volumeMounts": [ 861 | { 862 | "mountPath": "/etc/ssl/certs", 863 | "name": "ca-certs", 864 | "readOnly": true 865 | }, 866 | { 867 | "mountPath": "/etc/ca-certificates", 868 | "name": "etc-ca-certificates", 869 | "readOnly": true 870 | }, 871 | { 872 | "mountPath": "/var/lib/minikube/certs", 873 | "name": "k8s-certs", 874 | "readOnly": true 875 | }, 876 | { 877 | "mountPath": "/usr/local/share/ca-certificates", 878 | "name": "usr-local-share-ca-certificates", 879 | "readOnly": true 880 | }, 881 | { 882 | "mountPath": "/usr/share/ca-certificates", 883 | "name": "usr-share-ca-certificates", 884 | "readOnly": true 885 | } 886 | ] 887 | } 888 | ], 889 | "conditions": [ 890 | { 891 | "lastProbeTime": null, 892 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 893 | "status": "True", 894 | "type": "Initialized" 895 | }, 896 | { 897 | "lastProbeTime": null, 898 | "lastTransitionTime": "2023-09-13T12:16:32.000Z", 899 | "status": "True", 900 | "type": "Ready" 901 | }, 902 | { 903 | "lastProbeTime": null, 904 | "lastTransitionTime": "2023-09-13T12:16:32.000Z", 905 | "status": "True", 906 | "type": "ContainersReady" 907 | }, 908 | { 909 | "lastProbeTime": null, 910 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 911 | "status": "True", 912 | "type": "PodScheduled" 913 | } 914 | ], 915 | "containerStatuses": [ 916 | { 917 | "containerID": "docker://9ac7afc8a53d2f41bc781bdde3de98c12c53a382dd0b740e68fead9c7d675826", 918 | "image": "registry.k8s.io/kube-apiserver:v1.27.4", 919 | "imageID": "docker-pullable://registry.k8s.io/kube-apiserver@sha256:697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d", 920 | "lastState": {}, 921 | "name": "kube-apiserver", 922 | "ready": true, 923 | "restartCount": 0, 924 | "started": true, 925 | "state": { 926 | "running": { 927 | "startedAt": "2023-09-13T12:15:50.000Z" 928 | } 929 | } 930 | } 931 | ], 932 | "phase": "Running", 933 | "podIP": "192.168.49.2" 934 | }, 935 | { 936 | "name": "kube-controller-manager-minikube", 937 | "namespace": "kube-system", 938 | "uid": "957c83dd-cda3-47d2-8f49-cea7f6847f87", 939 | "creationTimestamp": "2023-09-13T12:16:25.000Z", 940 | "labels": { 941 | "component": "kube-controller-manager", 942 | "tier": "control-plane" 943 | }, 944 | "nodeName": "minikube", 945 | "containers": [ 946 | { 947 | "command": [ 948 | "kube-controller-manager", 949 | "--allocate-node-cidrs=true", 950 | "--authentication-kubeconfig=/etc/kubernetes/controller-manager.conf", 951 | "--authorization-kubeconfig=/etc/kubernetes/controller-manager.conf", 952 | "--bind-address=127.0.0.1", 953 | "--client-ca-file=/var/lib/minikube/certs/ca.crt", 954 | "--cluster-cidr=10.244.0.0/16", 955 | "--cluster-name=mk", 956 | "--cluster-signing-cert-file=/var/lib/minikube/certs/ca.crt", 957 | "--cluster-signing-key-file=/var/lib/minikube/certs/ca.key", 958 | "--controllers=*,bootstrapsigner,tokencleaner", 959 | "--kubeconfig=/etc/kubernetes/controller-manager.conf", 960 | "--leader-elect=false", 961 | "--requestheader-client-ca-file=/var/lib/minikube/certs/front-proxy-ca.crt", 962 | "--root-ca-file=/var/lib/minikube/certs/ca.crt", 963 | "--service-account-private-key-file=/var/lib/minikube/certs/sa.key", 964 | "--service-cluster-ip-range=10.96.0.0/12", 965 | "--use-service-account-credentials=true" 966 | ], 967 | "image": "registry.k8s.io/kube-controller-manager:v1.27.4", 968 | "imagePullPolicy": "IfNotPresent", 969 | "livenessProbe": { 970 | "failureThreshold": 8, 971 | "httpGet": { 972 | "host": "127.0.0.1", 973 | "path": "/healthz", 974 | "port": 10257, 975 | "scheme": "HTTPS" 976 | }, 977 | "initialDelaySeconds": 10, 978 | "periodSeconds": 10, 979 | "successThreshold": 1, 980 | "timeoutSeconds": 15 981 | }, 982 | "name": "kube-controller-manager", 983 | "resources": { 984 | "requests": { 985 | "cpu": "200m" 986 | } 987 | }, 988 | "startupProbe": { 989 | "failureThreshold": 24, 990 | "httpGet": { 991 | "host": "127.0.0.1", 992 | "path": "/healthz", 993 | "port": 10257, 994 | "scheme": "HTTPS" 995 | }, 996 | "initialDelaySeconds": 10, 997 | "periodSeconds": 10, 998 | "successThreshold": 1, 999 | "timeoutSeconds": 15 1000 | }, 1001 | "terminationMessagePath": "/dev/termination-log", 1002 | "terminationMessagePolicy": "File", 1003 | "volumeMounts": [ 1004 | { 1005 | "mountPath": "/etc/ssl/certs", 1006 | "name": "ca-certs", 1007 | "readOnly": true 1008 | }, 1009 | { 1010 | "mountPath": "/etc/ca-certificates", 1011 | "name": "etc-ca-certificates", 1012 | "readOnly": true 1013 | }, 1014 | { 1015 | "mountPath": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec", 1016 | "name": "flexvolume-dir" 1017 | }, 1018 | { 1019 | "mountPath": "/var/lib/minikube/certs", 1020 | "name": "k8s-certs", 1021 | "readOnly": true 1022 | }, 1023 | { 1024 | "mountPath": "/etc/kubernetes/controller-manager.conf", 1025 | "name": "kubeconfig", 1026 | "readOnly": true 1027 | }, 1028 | { 1029 | "mountPath": "/usr/local/share/ca-certificates", 1030 | "name": "usr-local-share-ca-certificates", 1031 | "readOnly": true 1032 | }, 1033 | { 1034 | "mountPath": "/usr/share/ca-certificates", 1035 | "name": "usr-share-ca-certificates", 1036 | "readOnly": true 1037 | } 1038 | ] 1039 | } 1040 | ], 1041 | "conditions": [ 1042 | { 1043 | "lastProbeTime": null, 1044 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 1045 | "status": "True", 1046 | "type": "Initialized" 1047 | }, 1048 | { 1049 | "lastProbeTime": null, 1050 | "lastTransitionTime": "2023-09-13T12:16:33.000Z", 1051 | "status": "True", 1052 | "type": "Ready" 1053 | }, 1054 | { 1055 | "lastProbeTime": null, 1056 | "lastTransitionTime": "2023-09-13T12:16:33.000Z", 1057 | "status": "True", 1058 | "type": "ContainersReady" 1059 | }, 1060 | { 1061 | "lastProbeTime": null, 1062 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 1063 | "status": "True", 1064 | "type": "PodScheduled" 1065 | } 1066 | ], 1067 | "containerStatuses": [ 1068 | { 1069 | "containerID": "docker://30a8e9da6be7bc6e7c0c9afdb802931b2ca94186705c6f2bba67f9d369d558b2", 1070 | "image": "registry.k8s.io/kube-controller-manager:v1.27.4", 1071 | "imageID": "docker-pullable://registry.k8s.io/kube-controller-manager@sha256:6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265", 1072 | "lastState": {}, 1073 | "name": "kube-controller-manager", 1074 | "ready": true, 1075 | "restartCount": 0, 1076 | "started": true, 1077 | "state": { 1078 | "running": { 1079 | "startedAt": "2023-09-13T12:15:51.000Z" 1080 | } 1081 | } 1082 | } 1083 | ], 1084 | "phase": "Running", 1085 | "podIP": "192.168.49.2" 1086 | }, 1087 | { 1088 | "name": "kube-proxy-sdcrs", 1089 | "namespace": "kube-system", 1090 | "uid": "0ed61e04-cc1c-4db3-ab4c-0d07ada7963d", 1091 | "creationTimestamp": "2023-09-13T12:16:41.000Z", 1092 | "labels": { 1093 | "controller-revision-hash": "86cc8bcbf7", 1094 | "k8s-app": "kube-proxy", 1095 | "pod-template-generation": "1" 1096 | }, 1097 | "nodeName": "minikube", 1098 | "containers": [ 1099 | { 1100 | "command": [ 1101 | "/usr/local/bin/kube-proxy", 1102 | "--config=/var/lib/kube-proxy/config.conf", 1103 | "--hostname-override=$(NODE_NAME)" 1104 | ], 1105 | "env": [ 1106 | { 1107 | "name": "NODE_NAME", 1108 | "valueFrom": { 1109 | "fieldRef": { 1110 | "apiVersion": "v1", 1111 | "fieldPath": "spec.nodeName" 1112 | } 1113 | } 1114 | } 1115 | ], 1116 | "image": "registry.k8s.io/kube-proxy:v1.27.4", 1117 | "imagePullPolicy": "IfNotPresent", 1118 | "name": "kube-proxy", 1119 | "resources": {}, 1120 | "securityContext": { 1121 | "privileged": true 1122 | }, 1123 | "terminationMessagePath": "/dev/termination-log", 1124 | "terminationMessagePolicy": "File", 1125 | "volumeMounts": [ 1126 | { 1127 | "mountPath": "/var/lib/kube-proxy", 1128 | "name": "kube-proxy" 1129 | }, 1130 | { 1131 | "mountPath": "/run/xtables.lock", 1132 | "name": "xtables-lock" 1133 | }, 1134 | { 1135 | "mountPath": "/lib/modules", 1136 | "name": "lib-modules", 1137 | "readOnly": true 1138 | }, 1139 | { 1140 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 1141 | "name": "kube-api-access-fphwt", 1142 | "readOnly": true 1143 | } 1144 | ] 1145 | } 1146 | ], 1147 | "serviceAccount": "kube-proxy", 1148 | "conditions": [ 1149 | { 1150 | "lastProbeTime": null, 1151 | "lastTransitionTime": "2023-09-13T12:16:42.000Z", 1152 | "status": "True", 1153 | "type": "Initialized" 1154 | }, 1155 | { 1156 | "lastProbeTime": null, 1157 | "lastTransitionTime": "2023-09-13T12:16:48.000Z", 1158 | "status": "True", 1159 | "type": "Ready" 1160 | }, 1161 | { 1162 | "lastProbeTime": null, 1163 | "lastTransitionTime": "2023-09-13T12:16:48.000Z", 1164 | "status": "True", 1165 | "type": "ContainersReady" 1166 | }, 1167 | { 1168 | "lastProbeTime": null, 1169 | "lastTransitionTime": "2023-09-13T12:16:42.000Z", 1170 | "status": "True", 1171 | "type": "PodScheduled" 1172 | } 1173 | ], 1174 | "containerStatuses": [ 1175 | { 1176 | "containerID": "docker://77f3f27ee56c646ce3dd3f1167f3758adfea5d29c8938e69ace5cdb732257b17", 1177 | "image": "registry.k8s.io/kube-proxy:v1.27.4", 1178 | "imageID": "docker-pullable://registry.k8s.io/kube-proxy@sha256:4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf", 1179 | "lastState": {}, 1180 | "name": "kube-proxy", 1181 | "ready": true, 1182 | "restartCount": 0, 1183 | "started": true, 1184 | "state": { 1185 | "running": { 1186 | "startedAt": "2023-09-13T12:16:48.000Z" 1187 | } 1188 | } 1189 | } 1190 | ], 1191 | "phase": "Running", 1192 | "podIP": "192.168.49.2" 1193 | }, 1194 | { 1195 | "name": "kube-scheduler-minikube", 1196 | "namespace": "kube-system", 1197 | "uid": "ea121d7d-22ef-473c-83c8-f402a761ff13", 1198 | "creationTimestamp": "2023-09-13T12:16:25.000Z", 1199 | "labels": { 1200 | "component": "kube-scheduler", 1201 | "tier": "control-plane" 1202 | }, 1203 | "nodeName": "minikube", 1204 | "containers": [ 1205 | { 1206 | "command": [ 1207 | "kube-scheduler", 1208 | "--authentication-kubeconfig=/etc/kubernetes/scheduler.conf", 1209 | "--authorization-kubeconfig=/etc/kubernetes/scheduler.conf", 1210 | "--bind-address=127.0.0.1", 1211 | "--kubeconfig=/etc/kubernetes/scheduler.conf", 1212 | "--leader-elect=false" 1213 | ], 1214 | "image": "registry.k8s.io/kube-scheduler:v1.27.4", 1215 | "imagePullPolicy": "IfNotPresent", 1216 | "livenessProbe": { 1217 | "failureThreshold": 8, 1218 | "httpGet": { 1219 | "host": "127.0.0.1", 1220 | "path": "/healthz", 1221 | "port": 10259, 1222 | "scheme": "HTTPS" 1223 | }, 1224 | "initialDelaySeconds": 10, 1225 | "periodSeconds": 10, 1226 | "successThreshold": 1, 1227 | "timeoutSeconds": 15 1228 | }, 1229 | "name": "kube-scheduler", 1230 | "resources": { 1231 | "requests": { 1232 | "cpu": "100m" 1233 | } 1234 | }, 1235 | "startupProbe": { 1236 | "failureThreshold": 24, 1237 | "httpGet": { 1238 | "host": "127.0.0.1", 1239 | "path": "/healthz", 1240 | "port": 10259, 1241 | "scheme": "HTTPS" 1242 | }, 1243 | "initialDelaySeconds": 10, 1244 | "periodSeconds": 10, 1245 | "successThreshold": 1, 1246 | "timeoutSeconds": 15 1247 | }, 1248 | "terminationMessagePath": "/dev/termination-log", 1249 | "terminationMessagePolicy": "File", 1250 | "volumeMounts": [ 1251 | { 1252 | "mountPath": "/etc/kubernetes/scheduler.conf", 1253 | "name": "kubeconfig", 1254 | "readOnly": true 1255 | } 1256 | ] 1257 | } 1258 | ], 1259 | "conditions": [ 1260 | { 1261 | "lastProbeTime": null, 1262 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 1263 | "status": "True", 1264 | "type": "Initialized" 1265 | }, 1266 | { 1267 | "lastProbeTime": null, 1268 | "lastTransitionTime": "2023-09-13T12:16:32.000Z", 1269 | "status": "True", 1270 | "type": "Ready" 1271 | }, 1272 | { 1273 | "lastProbeTime": null, 1274 | "lastTransitionTime": "2023-09-13T12:16:32.000Z", 1275 | "status": "True", 1276 | "type": "ContainersReady" 1277 | }, 1278 | { 1279 | "lastProbeTime": null, 1280 | "lastTransitionTime": "2023-09-13T12:16:25.000Z", 1281 | "status": "True", 1282 | "type": "PodScheduled" 1283 | } 1284 | ], 1285 | "containerStatuses": [ 1286 | { 1287 | "containerID": "docker://4b1795b1275f7d71a911b73789016487bdada2e84d09de7a35663b7ad9c1af56", 1288 | "image": "registry.k8s.io/kube-scheduler:v1.27.4", 1289 | "imageID": "docker-pullable://registry.k8s.io/kube-scheduler@sha256:5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af", 1290 | "lastState": {}, 1291 | "name": "kube-scheduler", 1292 | "ready": true, 1293 | "restartCount": 0, 1294 | "started": true, 1295 | "state": { 1296 | "running": { 1297 | "startedAt": "2023-09-13T12:15:52.000Z" 1298 | } 1299 | } 1300 | } 1301 | ], 1302 | "phase": "Running", 1303 | "podIP": "192.168.49.2" 1304 | }, 1305 | { 1306 | "name": "storage-provisioner", 1307 | "namespace": "kube-system", 1308 | "uid": "7cc8f6f3-2ff6-43e3-a37a-280378f07b17", 1309 | "creationTimestamp": "2023-09-13T12:16:50.000Z", 1310 | "labels": { 1311 | "addonmanager.kubernetes.io/mode": "Reconcile", 1312 | "integration-test": "storage-provisioner" 1313 | }, 1314 | "nodeName": "minikube", 1315 | "containers": [ 1316 | { 1317 | "command": [ 1318 | "/storage-provisioner" 1319 | ], 1320 | "image": "gcr.io/k8s-minikube/storage-provisioner:v5", 1321 | "imagePullPolicy": "IfNotPresent", 1322 | "name": "storage-provisioner", 1323 | "resources": {}, 1324 | "terminationMessagePath": "/dev/termination-log", 1325 | "terminationMessagePolicy": "File", 1326 | "volumeMounts": [ 1327 | { 1328 | "mountPath": "/tmp", 1329 | "name": "tmp" 1330 | }, 1331 | { 1332 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", 1333 | "name": "kube-api-access-rjb5t", 1334 | "readOnly": true 1335 | } 1336 | ] 1337 | } 1338 | ], 1339 | "serviceAccount": "storage-provisioner", 1340 | "conditions": [ 1341 | { 1342 | "lastProbeTime": null, 1343 | "lastTransitionTime": "2023-09-13T12:16:50.000Z", 1344 | "status": "True", 1345 | "type": "Initialized" 1346 | }, 1347 | { 1348 | "lastProbeTime": null, 1349 | "lastTransitionTime": "2023-09-13T12:33:39.000Z", 1350 | "status": "True", 1351 | "type": "Ready" 1352 | }, 1353 | { 1354 | "lastProbeTime": null, 1355 | "lastTransitionTime": "2023-09-13T12:33:39.000Z", 1356 | "status": "True", 1357 | "type": "ContainersReady" 1358 | }, 1359 | { 1360 | "lastProbeTime": null, 1361 | "lastTransitionTime": "2023-09-13T12:16:50.000Z", 1362 | "status": "True", 1363 | "type": "PodScheduled" 1364 | } 1365 | ], 1366 | "containerStatuses": [ 1367 | { 1368 | "containerID": "docker://f0c9d1d02f3c98b365ec14d93b641ce8c3719d5da6b426d97a06cbd9f4ea3959", 1369 | "image": "gcr.io/k8s-minikube/storage-provisioner:v5", 1370 | "imageID": "docker-pullable://gcr.io/k8s-minikube/storage-provisioner@sha256:18eb69d1418e854ad5a19e399310e52808a8321e4c441c1dddad8977a0d7a944", 1371 | "lastState": { 1372 | "terminated": { 1373 | "containerID": "docker://0e035141fd5b54114bd8169e2858796445c4473d2b5e39b7dd38c48d330c9fd4", 1374 | "exitCode": 255, 1375 | "finishedAt": "2023-09-13T12:33:23.000Z", 1376 | "reason": "Error", 1377 | "startedAt": "2023-09-13T12:16:52.000Z" 1378 | } 1379 | }, 1380 | "name": "storage-provisioner", 1381 | "ready": true, 1382 | "restartCount": 1, 1383 | "started": true, 1384 | "state": { 1385 | "running": { 1386 | "startedAt": "2023-09-13T12:33:38.000Z" 1387 | } 1388 | } 1389 | } 1390 | ], 1391 | "phase": "Running", 1392 | "podIP": "192.168.49.2" 1393 | } 1394 | ], 1395 | "namespaces": [ 1396 | { 1397 | "name": "default", 1398 | "uid": "55333ff2-133f-499b-8c6b-cfa6bdca3c72", 1399 | "creationTimestamp": "2023-09-13T12:16:15.000Z", 1400 | "labels": { 1401 | "kubernetes.io/metadata.name": "default" 1402 | }, 1403 | "phase": "Active", 1404 | "nodeName": "" 1405 | }, 1406 | { 1407 | "name": "kube-node-lease", 1408 | "uid": "182e850e-e809-488e-ae8b-aea3298b7a48", 1409 | "creationTimestamp": "2023-09-13T12:16:15.000Z", 1410 | "labels": { 1411 | "kubernetes.io/metadata.name": "kube-node-lease" 1412 | }, 1413 | "phase": "Active", 1414 | "nodeName": "" 1415 | }, 1416 | { 1417 | "name": "kube-public", 1418 | "uid": "5f094858-a0ee-44e8-975b-a4b285c559f1", 1419 | "creationTimestamp": "2023-09-13T12:16:15.000Z", 1420 | "labels": { 1421 | "kubernetes.io/metadata.name": "kube-public" 1422 | }, 1423 | "phase": "Active", 1424 | "nodeName": "" 1425 | }, 1426 | { 1427 | "name": "kube-system", 1428 | "uid": "bbffaa98-c820-46e7-839e-ef14176b0f08", 1429 | "creationTimestamp": "2023-09-13T12:16:15.000Z", 1430 | "labels": { 1431 | "kubernetes.io/metadata.name": "kube-system" 1432 | }, 1433 | "phase": "Active", 1434 | "nodeName": "" 1435 | } 1436 | ], 1437 | "services": [ 1438 | { 1439 | "name": "kubernetes", 1440 | "namespace": "default", 1441 | "uid": "0d491f5d-ce16-4075-b44a-43776a5157a7", 1442 | "creationTimestamp": "2023-09-13T12:16:21.000Z", 1443 | "labels": { 1444 | "component": "apiserver", 1445 | "provider": "kubernetes" 1446 | }, 1447 | "ports": [ 1448 | { 1449 | "name": "https", 1450 | "port": 443, 1451 | "protocol": "TCP", 1452 | "targetPort": 8443 1453 | } 1454 | ], 1455 | "loadBalancer": {}, 1456 | "clusterIP": "10.96.0.1" 1457 | }, 1458 | { 1459 | "name": "kube-dns", 1460 | "namespace": "kube-system", 1461 | "uid": "32bf078c-1658-4651-b964-ecfedd31893d", 1462 | "creationTimestamp": "2023-09-13T12:16:24.000Z", 1463 | "labels": { 1464 | "k8s-app": "kube-dns", 1465 | "kubernetes.io/cluster-service": "true", 1466 | "kubernetes.io/name": "CoreDNS" 1467 | }, 1468 | "ports": [ 1469 | { 1470 | "name": "dns", 1471 | "port": 53, 1472 | "protocol": "UDP", 1473 | "targetPort": 53 1474 | }, 1475 | { 1476 | "name": "dns-tcp", 1477 | "port": 53, 1478 | "protocol": "TCP", 1479 | "targetPort": 53 1480 | }, 1481 | { 1482 | "name": "metrics", 1483 | "port": 9153, 1484 | "protocol": "TCP", 1485 | "targetPort": 9153 1486 | } 1487 | ], 1488 | "loadBalancer": {}, 1489 | "clusterIP": "10.96.0.10" 1490 | } 1491 | ], 1492 | "deployments": [ 1493 | { 1494 | "name": "coredns", 1495 | "namespace": "kube-system", 1496 | "uid": "d500d0eb-3030-4a26-b30d-ff76869f5c7c", 1497 | "creationTimestamp": "2023-09-13T12:16:24.000Z", 1498 | "labels": { 1499 | "k8s-app": "kube-dns" 1500 | }, 1501 | "replicas": 1 1502 | } 1503 | ] 1504 | } 1505 | 1506 | 1507 | 1508 | */ -------------------------------------------------------------------------------- /server/controllers/postgraphileController.js: -------------------------------------------------------------------------------- 1 | // NOT IMPLEMENTED IN CURRENT VERSION. 2 | 3 | const express = require('express'); 4 | const { postgraphile } = require('postgraphile'); 5 | 6 | const app = express(); 7 | 8 | app.use(postgraphile( 9 | 'postgres://postgres:cincitychilli@localhost/fylakas-database', 10 | 'public', // schema name or array of schema names 11 | { 12 | watchPg: true, 13 | graphiql: true, // allows you to use graphQL with whatever db you're using 14 | enhanceGraphiql: true, // gives us more features 15 | } 16 | )); 17 | 18 | app.listen(5000); 19 | -------------------------------------------------------------------------------- /server/controllers/promController.js: -------------------------------------------------------------------------------- 1 | const db = require('../db/database.js'); 2 | const PromController = {}; 3 | 4 | // Get Prometheus URL for any given data request 5 | PromController.getEndpoint = async (req, res, next) => { 6 | const { username } = res.locals; 7 | if (!res.locals.isLoggedIn) return next(); 8 | try { 9 | // Find userID associated with stored username 10 | const userIdQuery = `SELECT user_id FROM public.users WHERE username = '${username}';`; 11 | const result = await db.query(userIdQuery); 12 | const userId = result.rows[0].user_id; 13 | // Find all endpoints associated with stored userID 14 | const endpointQuery = `SELECT prom_url FROM public.clusters WHERE user_id = '${userId}';`; 15 | const endpoints = await db.query(endpointQuery); 16 | // Store the most recently added endpoint on res.locals 17 | if (endpoints.rows[0] !== undefined) { 18 | res.locals.prometheusUrl = endpoints.rows[endpoints.rows.length - 1].prom_url; 19 | console.log(`Fetching data from "${res.locals.prometheusUrl}" for "${username}".`); 20 | } else { 21 | // If none exists, disable the following middleware by setting isloggedIn to false 22 | res.locals.isLoggedIn = false; 23 | console.log(`Cannot fetch data for "${username}" without a request URL.`); 24 | } 25 | return next(); 26 | } catch (err) { 27 | return next({ 28 | log: err, 29 | message: {err: 'Error occurred requesting endpoint for Prometheus.'}, 30 | }) 31 | } 32 | } 33 | 34 | // Store the current date and initialize metrics object 35 | PromController.getDate = function (req, res, next) { 36 | try { 37 | res.locals.metrics = {}; 38 | res.locals.metrics.date = Date.now(); 39 | return next(); 40 | } catch (err) { 41 | return next({ 42 | log: err, 43 | message: { err: 'Error occurred adding timeStamp to metrics.' }, 44 | }); 45 | } 46 | }; 47 | 48 | // Get cpu usage by container and add to the metrics object on res.locals 49 | PromController.cpuUsageByContainer = async function (req, res, next) { 50 | const {isLoggedIn, prometheusUrl} = res.locals; 51 | if (!isLoggedIn) return next(); 52 | // PromQL query 53 | const query = '(100 * sum(rate(node_cpu_seconds_total{mode="user"}[5m])) by (cluster)) / (sum(rate(node_cpu_seconds_total[5m])) by (cluster))'; 54 | 55 | // Construct the URL for the query 56 | const queryUrl = `${prometheusUrl}/api/v1/query?query=${encodeURIComponent(query)}`; 57 | 58 | try { 59 | const response = await fetch(queryUrl, { 60 | method: 'GET', 61 | headers: { 62 | Accept: 'application/json', 63 | 'Content-Type': 'application/json', 64 | }, 65 | }); 66 | 67 | // Make sure to throw new Error, not return Error 68 | if (!response.ok) { 69 | console.log('Response to GET request to prometheus server was not ok'); 70 | throw new Error('Response error'); 71 | } 72 | 73 | const queryData = await response.json(); 74 | 75 | if (queryData.data.result[0]) 76 | res.locals.metrics.cpu = queryData.data.result[0].value[1]; 77 | 78 | return next(); 79 | } catch (err) { 80 | return next({ 81 | log: err, 82 | message: { err: 'Error occurred adding cpuUsage to metrics.' }, 83 | }); 84 | } 85 | }; 86 | 87 | // Get memory usage by container and add to the metrics object on res.locals 88 | PromController.memoryUsageByContainer = async function (req, res, next) { 89 | const {isLoggedIn, prometheusUrl} = res.locals; 90 | if (!isLoggedIn) return next(); 91 | // declare specific query for memory usage for each container 92 | const query = 93 | '100 * sum(container_memory_usage_bytes) / sum(container_spec_memory_limit_bytes)'; 94 | // 'container_memory_usage_bytes'; 95 | // Construct the URL for the query 96 | const queryUrl = `${prometheusUrl}/api/v1/query?query=${encodeURIComponent( 97 | query 98 | )}`; 99 | 100 | try { 101 | const response = await fetch(queryUrl, { 102 | method: 'GET', 103 | headers: { 104 | Accept: 'application/json', 105 | 'Content-Type': 'application/json', 106 | }, 107 | }); 108 | // if the response is not ok, throw a new error referencing this middleware 109 | if (!response.ok) { 110 | throw new Error( 111 | 'Response error in PromController.memoryUsageByContainer' 112 | ); 113 | } 114 | 115 | const queryData = await response.json(); 116 | 117 | // assign memoryUsageByContainer as a property on res.locals.metrics assigned to the data received from the fetch 118 | if (queryData.data.result[0]) 119 | res.locals.metrics.mem = queryData.data.result[0].value[1]; 120 | return next(); 121 | } catch (err) { 122 | return next({ 123 | log: err, 124 | message: { err: 'Error occurred adding memoryUsage to metrics.' }, 125 | }); 126 | } 127 | }; 128 | 129 | // CURRENTLY UNUSED: Get network traffic by container and add to the metrics object on res.locals 130 | PromController.networkTrafficByContainer = async function (req, res, next) { 131 | const {isLoggedIn, prometheusUrl} = res.locals; 132 | if (!isLoggedIn) return next(); 133 | // PromQL queries 134 | const receiveQuery = 135 | '(rate(container_network_receive_bytes_total[5m]) / 1e9) * 100'; 136 | const transmitQuery = 137 | '(rate(container_network_receive_bytes_total[5m]) / 1e9) * 100'; 138 | // Construct the URL for the queries 139 | const receiveQueryUrl = `${prometheusUrl}/api/v1/query?query=${encodeURIComponent( 140 | receiveQuery 141 | )}`; 142 | const transmitQueryUrl = `${prometheusUrl}/api/v1/query?query=${encodeURIComponent( 143 | transmitQuery 144 | )}`; 145 | 146 | try { 147 | const [receiveResponse, transmitResponse] = await Promise.all([ 148 | fetch(receiveQueryUrl, { 149 | method: 'GET', 150 | headers: { 151 | Accept: 'application/json', 152 | 'Content-Type': 'application/json', 153 | }, 154 | }), 155 | fetch(transmitQueryUrl, { 156 | method: 'GET', 157 | headers: { 158 | Accept: 'application/json', 159 | 'Content-Type': 'application/json', 160 | }, 161 | }), 162 | ]); 163 | 164 | // Check if both responses are OK 165 | if (!receiveResponse.ok || !transmitResponse.ok) { 166 | console.log('Response to GET request was not ok'); 167 | throw new Error('Response error'); 168 | } 169 | 170 | const [receiveData, transmitData] = await Promise.all([ 171 | receiveResponse.json(), 172 | transmitResponse.json(), 173 | ]); 174 | 175 | // console.log('Received Data from server:', receiveData); 176 | // console.log('Transmitted Data from server:', transmitData); 177 | 178 | res.locals.metrics.networkTrafficByContainer = { 179 | received: receiveData, 180 | transmitted: transmitData, 181 | }; 182 | 183 | return next(); 184 | } catch (err) { 185 | return next({ 186 | log: err, 187 | message: { err: 'Error occurred adding networkTraffic to metrics.' }, 188 | }); 189 | } 190 | }; 191 | 192 | // Get disk usage by container and add to the metrics object on res.locals 193 | PromController.diskSpace = async function (req, res, next) { 194 | const {isLoggedIn, prometheusUrl} = res.locals; 195 | if (!isLoggedIn) return next(); 196 | //PromQL query for finding free space, gives percentage of available space on a pointed disk 197 | const query = 198 | //used disk space 199 | '100 - ((node_filesystem_avail_bytes{mountpoint="/",fstype!="rootfs"} * 100) / node_filesystem_size_bytes{mountpoint="/",fstype!="rootfs"})'; 200 | //free disk space 201 | // '100 * (node_filesystem_avail_bytes{mountpoint="/",fstype!="rootfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!="rootfs"})'; 202 | 203 | //Construct the URL for the query 204 | const queryUrl = `${prometheusUrl}/api/v1/query?query=${encodeURIComponent( 205 | query 206 | )}`; 207 | 208 | try { 209 | const response = await fetch(queryUrl, { 210 | method: 'GET', 211 | headers: { 212 | Accept: 'application/json', 213 | 'Content-Type': 'application/json', 214 | }, 215 | }); 216 | //Throw new error 217 | if (!response.ok) { 218 | console.log('Response to GET request to prometheus server was not ok'); 219 | throw new Error('Response error'); 220 | } 221 | 222 | const queryData = await response.json(); 223 | 224 | if (queryData.data.result[0]) 225 | res.locals.metrics.disk = queryData.data.result[0].value[1]; 226 | 227 | return next(); 228 | } catch (err) { 229 | return next({ 230 | log: err, 231 | message: { err: 'Error occurred adding diskSpace to metrics.' }, 232 | }); 233 | } 234 | }; 235 | 236 | // Add new Prometheus URL for a given account 237 | PromController.addEndpoint = async (req, res, next) => { 238 | const { promURL } = req.body 239 | const { username, isLoggedIn } = res.locals; 240 | // Reject request if user is not logged in 241 | if (!isLoggedIn) { 242 | res.locals.success = false; 243 | return next(); 244 | } 245 | try { 246 | // Find userID associated with stored username 247 | const userIdQuery = `SELECT user_id FROM public.users WHERE username = '${username}'` 248 | const result = await db.query(userIdQuery) 249 | const userId = result.rows[0].user_id; 250 | console.log(`Adding endpoint "${promURL}" for user "${username}".`); 251 | // Add endpoint to clusters table linked to the userID 252 | const endpointQuery = `INSERT INTO public.clusters (user_id, prom_url) VALUES ('${userId}', '${promURL}');` 253 | await db.query(endpointQuery); 254 | res.locals.success = true; 255 | return next(); 256 | } catch (err) { 257 | return next({ 258 | log: err, 259 | message: { err: 'Error occurred adding Prometheus endpoint to database.'} 260 | }) 261 | } 262 | } 263 | 264 | module.exports = PromController; 265 | -------------------------------------------------------------------------------- /server/db/database.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const PG_URI = process.env.pg_uri; 3 | 4 | const pool = new Pool({ 5 | connectionString: PG_URI, 6 | }); 7 | 8 | module.exports = { 9 | query: (text, params, callback) => { 10 | return pool.query(text, params, callback); 11 | }, 12 | end: () => pool.end(), 13 | }; 14 | -------------------------------------------------------------------------------- /server/models/UserModel.js: -------------------------------------------------------------------------------- 1 | // CURRENTLY UNIMPLEMENTED. 2 | 3 | /**const express = require('express'); 4 | const { Client } = require('pg'); 5 | const app = express(); 6 | const port = 3000; 7 | 8 | const client = new Client({ 9 | host: 'localhost', 10 | port: 5432, 11 | user: 'your_username_here', 12 | password: 'your_password_here', 13 | database: 'your_database_here' 14 | }); 15 | 16 | client.connect(); 17 | 18 | app.get('/', (req, res) => { 19 | res.send('Hello, World!'); 20 | }); 21 | 22 | app.get('/db', async (req, res) => { 23 | try { 24 | const result = await client.query('SELECT * FROM your_table_here'); 25 | res.json(result.rows); 26 | } catch (err) { 27 | console.error(err); 28 | res.send('Error fetching data from database'); 29 | } 30 | }); 31 | 32 | app.listen(port, () => { 33 | console.log(`App running on port ${port}.`); 34 | }); */ -------------------------------------------------------------------------------- /server/routers/K8srouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const k8sController = require('../controllers/k8sController.js'); 3 | 4 | const k8srouter = express.Router(); 5 | 6 | k8srouter.get( 7 | '/cluster', 8 | k8sController.getNodes, 9 | k8sController.getPods, 10 | k8sController.getNamespaces, 11 | k8sController.getServices, 12 | k8sController.getDeployments, 13 | k8sController.getCluster, 14 | k8sController.nodeStatus, 15 | k8sController.podStatus, 16 | async (_, res) => { 17 | return res.status(200).json(res.locals.cluster); 18 | }, 19 | ); 20 | 21 | module.exports = k8srouter; -------------------------------------------------------------------------------- /server/routers/authRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authController = require('../controllers/authController.js'); 3 | const router = express.Router(); 4 | 5 | router.post( 6 | '/login', 7 | authController.handleUserDetails, 8 | authController.login, 9 | authController.startSession, 10 | (req, res) => { 11 | res.status(200).json({ profile: res.locals.profile }); 12 | } 13 | ); 14 | 15 | router.post( 16 | '/signup', 17 | authController.handleUserDetails, 18 | authController.signup, 19 | authController.startSession, 20 | (req, res) => { 21 | res.status(200).json({ profile: res.locals.profile }); 22 | } 23 | ); 24 | 25 | router.get('/check', authController.isLoggedIn, (req, res) => { 26 | res.status(200).json({ isLoggedIn: res.locals.isLoggedIn }); 27 | }); 28 | 29 | router.get('/logout', authController.logout, (req, res) => { 30 | res.status(200).json({ message: 'Successfully logged out' }); 31 | }); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /server/routers/promRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const PromController = require('../controllers/promController.js'); 3 | const authController = require('../controllers/authController.js'); 4 | 5 | const promRouter = express.Router(); 6 | 7 | promRouter.use(authController.isLoggedIn); 8 | 9 | promRouter.post( 10 | '/metrics', 11 | PromController.getEndpoint, 12 | PromController.getDate, 13 | PromController.cpuUsageByContainer, 14 | PromController.memoryUsageByContainer, 15 | PromController.diskSpace, 16 | async (_, res) => { 17 | console.log(`Sending res.locals.metrics to ${res.locals.username}.`); 18 | return res.status(200).json(res.locals.metrics); 19 | } 20 | ); 21 | 22 | promRouter.post( 23 | '/endpoint', 24 | PromController.addEndpoint, 25 | (_, res) => { 26 | return res.status(200).json({success: res.locals.success}); 27 | } 28 | ); 29 | 30 | module.exports = promRouter; 31 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | 5 | const app = express(); 6 | const PORT = process.env.PORT || 3000; // Default to 3000 if no environment variable is set 7 | 8 | // Routers 9 | const authRouter = require('./routers/authRouter.js'); 10 | const promRouter = require('./routers/promRouter.js'); 11 | 12 | // Parse request body 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: true })); 15 | app.use(cookieParser()); 16 | 17 | // Handle static files 18 | app.use('/', express.static(path.resolve(__dirname, '../build'))); 19 | app.use('/assets', express.static(path.resolve(__dirname, '../assets'))); 20 | 21 | app.get('/', (req, res) => { 22 | return res.status(200).sendFile(path.join(__dirname, '../client/index.html')); 23 | }); 24 | 25 | // Use routers 26 | app.use('/api/auth', authRouter); 27 | app.use('/api/prom', promRouter); 28 | 29 | // Catch-all route handler for any requests to an unknown route 30 | app.use('*', (req, res) => 31 | res.status(404).send("This is not the page you're looking for...") 32 | ); 33 | 34 | // Global Error Handler 35 | app.use((err, req, res, next) => { 36 | const defaultErr = { 37 | log: 'Express error handler caught unknown middleware error', 38 | status: 500, 39 | message: { err: 'An error occurred' }, 40 | }; 41 | const errorObj = Object.assign({}, defaultErr, err); 42 | console.log(errorObj.log); 43 | return res.status(errorObj.status).json(errorObj.message); 44 | }); 45 | 46 | // Start server 47 | let server; 48 | if (require.main === module) { 49 | server = app.listen(PORT, () => { 50 | console.log(`Server listening on port: ${PORT}...`); 51 | }); 52 | } 53 | 54 | module.exports = { app, server }; 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | devServer: { 6 | //Chris added this serve for static files because webpack doesn't bundle on it's own? 7 | static: { 8 | directory: path.resolve(__dirname, './assets'), 9 | publicPath: '/assets' 10 | }, 11 | proxy: { 12 | '/api': 'http://localhost:3000' 13 | }, 14 | headers: {'Access-Control-Allow-Origin': '*'}, 15 | }, 16 | entry: path.join(__dirname, 'client/App.jsx'), 17 | output: { 18 | path: path.join(__dirname, 'build'), 19 | filename: 'bundle.js', 20 | }, 21 | mode: process.env.NODE_ENV, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/i, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'babel-loader', 29 | options: { 30 | presets: [ 31 | '@babel/preset-env', 32 | '@babel/preset-react', 33 | ], 34 | }, 35 | }, 36 | }, 37 | { 38 | test: /.s?[ac]ss$/i, 39 | // test: /\.s?css/, 40 | exclude: /node_modules/, 41 | use: [ 42 | 'style-loader', 43 | 'css-loader', 44 | 'sass-loader', 45 | ], 46 | }, 47 | { 48 | test: /\.(png|jpe?g|gif)$/i, 49 | // test: /\.png/i, 50 | use: [ 51 | { 52 | loader: 'file-loader', 53 | options: { 54 | name: '[name].[ext]', 55 | outputPath: 'images', // Optional: specify the output path 56 | }, 57 | }, 58 | ], 59 | } 60 | ], 61 | }, 62 | plugins: [ 63 | new HtmlWebpackPlugin({ 64 | template: path.join(__dirname, 'client/index.html'), 65 | }), // maybe a problem-child here 66 | ], 67 | resolve: { 68 | extensions: [ 69 | '.js', 70 | '.jsx', 71 | ], 72 | }, 73 | }; 74 | --------------------------------------------------------------------------------