├── .DS_Store ├── .github └── workflows │ └── testing.yml ├── .gitignore ├── LICENSE ├── README.md ├── __testing__ ├── backend.test.js └── frontend.test.js ├── babel.config.json ├── client ├── .DS_Store ├── index.html ├── index.js ├── src │ ├── .DS_Store │ ├── App.jsx │ ├── assets │ │ ├── AsclepiusLogoOld.png │ │ ├── ClickPod.gif │ │ ├── FullhorizontalAsclepius.png │ │ ├── Github_icon.png │ │ ├── LinkedIn_icon.svg.png │ │ ├── arrow.png │ │ ├── connectionflowONLY.gif │ │ ├── favicon.ico │ │ ├── headshots │ │ │ ├── cam.png │ │ │ ├── hugh.png │ │ │ ├── john.png │ │ │ ├── kola.png │ │ │ └── nick.png │ │ ├── hughcircle.png │ │ ├── k8sicon.png │ │ ├── password-visible-1.png │ │ └── populatedNodes.png │ ├── components │ │ ├── Chart.jsx │ │ ├── Cost.jsx │ │ ├── NodeMap.jsx │ │ ├── Prompt.jsx │ │ ├── Sidebarsection.jsx │ │ └── prompt │ │ │ ├── AwsCLIInst.jsx │ │ │ ├── AwsForm.jsx │ │ │ ├── AzCLIInst.jsx │ │ │ ├── ConnnectCluster.jsx │ │ │ ├── KubectlInst.jsx │ │ │ └── LocalInst.jsx │ ├── containers │ │ ├── .DS_Store │ │ ├── AsclepiusLogo.png │ │ ├── HeaderContainer.jsx │ │ ├── NodeMapContainer.jsx │ │ └── SideBarContainer.jsx │ └── redux │ │ ├── slices │ │ ├── nodeSlice.js │ │ └── userSlice.js │ │ └── store.js └── styles.scss ├── dist ├── 2a3905e4d4786d742b06b9cfba58a3a9.png ├── bundle.js ├── bundle.js.LICENSE.txt └── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── server ├── controllers │ ├── awsController.ts │ ├── azController.ts │ ├── dataController.ts │ └── dataControllerNew.ts ├── routes │ ├── awsRouter.ts │ ├── azRouter.ts │ └── dataRouter.ts ├── server.ts └── types.ts ├── tsconfig.json └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | run-name: ${{ github.actor }} made a pull request 3 | on: [pull_request] 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | node-version: ['16.x', '18.x'] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | 20 | - name: Cache Dependencies 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | 28 | - name: npm install 29 | run: npm ci 30 | 31 | - name: tests 32 | run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Asclepius

2 | 3 |

Kubernetes Cluster health monitoring tool

4 | 5 | 6 | html5 7 | 8 | 9 | html5 10 | 11 |
12 |
13 |
14 |

Tech Stack

15 |
16 | React 17 | Redux 18 | Nodejs 19 | Express 20 | PostgreSQL 21 | JavaScript 22 |
23 | Kubernetes 24 | html5 25 | css3 26 | Sass 27 | git 28 | Webpack 29 | D3 30 |
31 |
32 |
33 |

Introduction:

34 |

Asclepius is an open-source K8s node health monitoring service for local or cloud-deployed K8s clusters. Asclepius delivers a simplified dashboard of each K8s node’s health at a glance, with the option to select nodes and display the contained pod list with kubelet supplied data metrics. The Asclepius dashboard updates in near real time to ensure that as soon as any nodes show signs of going down, you and your team can respond accordingly.

35 |

How to use Asclepius:

36 |

Downloading the Source Code:

37 |

To get started with Asclepius, clone it onto your machine. After you've cloned the repository into the folder of your choice run:

38 | npm install 39 |

After installing the required dependencies, you should be able to run the command:

40 | npm start 41 |

This command will spin up the app and open a new page in your default browser; accessing "localhost:8080", where you should now see the Asclepius home page!

42 |

Once Asclepius is running in your browser, you have access to a button: "Render Node Map". From here, Asclepius makes it easy to connect either your local or cloud hosted cluster. Please follow the prompts and Asclepius will properly install required CLIs and apply any necessary configurations for you. 43 |

44 | html5 45 | 46 |
47 |

Engineering Philosophy:

48 |

Asclepius was created to make the user experience of visualizing node health as seamless and abstracted as possible. We accomplish this by guiding you through a series of prompts designed to successfully add a Kubernetes config file to your local machine. Asclepius currently supports local and cloud-hosted Kubernetes deployments. If you are using a cloud platform not supported by Asclepius, please research the necessary steps to get a config file on your system. After this is accomplished, you should also be able to visualize your cluster health using the "Render Node Map" button.

49 |
50 |

Asclepius interacts with your local terminal through a Node.js method called "spawnSync". As the user works through the series of prompts and checks on the client side, we call spawnSync for a variety of functionality including: version checks, login authentication, configuration and kubectl metric retrieval queries.

51 |

52 |
53 |
54 |

How to Contribute

55 |

56 |

Branch management

57 | Please submit any pull requests to the dev branch. All changes will be reviewed by the team before merging. 58 | 59 |

Bugs and suggestions

60 | For help with existing issues, please read our GitHub issues page. If you cannot find a relevant topic in the issues page, please file a new issue. 61 | We welcome all suggestions and feedback! 62 |

63 |

Meet the Team

64 |

Asclepius was created by a development team under the OS-Labs open source tech accelerator.

65 |
66 |

Kola Bamgbose 67 |

68 | 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 |

Cameron Blair 78 |

79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 |

John Norlin

89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 |
100 |

Hugh Stapleton

101 | 102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |

Nick Vanderlinden

111 | 112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 |
120 |
121 |
122 |

Current Version

123 |

Alpha: v0.1.0

124 |
125 |
126 | -------------------------------------------------------------------------------- /__testing__/backend.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from '../server/server'; 3 | 4 | describe('Server Route Testing', () => { 5 | 6 | describe('Unkown route test', () => { 7 | it('should return status 404 for GET to unknown endpoints', async () => { 8 | const res = await request(app).get('/nonexistantroute'); 9 | expect(res.statusCode).toBe(404); 10 | }); 11 | }); 12 | 13 | describe('Serving static files', () => { 14 | it('should return static files for root route', async() => { 15 | const res = await request(app).get('/'); 16 | expect(res.statusCode).toBe(200); 17 | expect(res.headers['content-type']).toMatch('text/html'); 18 | }) 19 | }) 20 | 21 | describe('Get Data Route', () => { 22 | 23 | // describe('GET request to /getdata', () => { 24 | // it('Should return false for no kubectl installed', async () => { 25 | // const res = await request(app).get('/getdata'); 26 | // expect(res.statusCode).toBe(200); 27 | // expect(res.body).toBe(false); 28 | // }); 29 | // }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /__testing__/frontend.test.js: -------------------------------------------------------------------------------- 1 | import userSliceSubject from '../client/src/redux/slices/userSlice'; 2 | import { kubectlSet, showPrompt } from '../client/src/redux/slices/userSlice'; 3 | import store from '../client/src/redux/store'; 4 | 5 | 6 | describe('Test Redux Reducers', () => { 7 | let state; 8 | 9 | beforeEach(() => { 10 | state = { 11 | kubectl: true, 12 | showPrompt: false, 13 | cloudInfo: false, 14 | localInfo: false, 15 | aksForm: false, 16 | aksInfo: { 17 | clusterName: "", 18 | resourceGroup: "", 19 | }, 20 | aksCLI: false 21 | } 22 | }) 23 | 24 | 25 | describe('default state action not defined', () => { 26 | it('should return default state if nothing is passed in', () => { 27 | expect(userSliceSubject(undefined, {type: undefined})).toEqual(state); 28 | }); 29 | }); 30 | 31 | describe('unrecognized action types', () => { 32 | it('should return the original with no duplicates', () => { 33 | const action = {type: 'kolaandhugh'}; 34 | expect(userSliceSubject(state, action)).toBe(state); 35 | }) 36 | }) 37 | 38 | describe('showPrompt action', () => { 39 | it ('should return the showPrompt property in state as changed', () => { 40 | const action = {type: showPrompt}; 41 | expect(userSliceSubject(state, action)).not.toBe(state); 42 | }) 43 | }) 44 | 45 | describe('kubectl action', () => { 46 | it ('should return the kubectl property in state as changed', () => { 47 | const action = {type: kubectlSet}; 48 | const newState = userSliceSubject(state, action) 49 | expect(newState).not.toEqual(state); 50 | }) 51 | }) 52 | }) -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/.DS_Store -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Asclepius 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./src/App.jsx"; 4 | import { store } from "./src/redux/store.js"; 5 | import { Provider } from "react-redux"; 6 | 7 | const container = document.getElementById("root"); 8 | const root = ReactDOM.createRoot(container); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /client/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/.DS_Store -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import NodeMapContainer from "./containers/NodeMapContainer.jsx"; 4 | import HeaderContainer from "./containers/HeaderContainer.jsx"; 5 | 6 | import "../styles.scss"; 7 | 8 | function App() { 9 | 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /client/src/assets/AsclepiusLogoOld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/AsclepiusLogoOld.png -------------------------------------------------------------------------------- /client/src/assets/ClickPod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/ClickPod.gif -------------------------------------------------------------------------------- /client/src/assets/FullhorizontalAsclepius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/FullhorizontalAsclepius.png -------------------------------------------------------------------------------- /client/src/assets/Github_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/Github_icon.png -------------------------------------------------------------------------------- /client/src/assets/LinkedIn_icon.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/LinkedIn_icon.svg.png -------------------------------------------------------------------------------- /client/src/assets/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/arrow.png -------------------------------------------------------------------------------- /client/src/assets/connectionflowONLY.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/connectionflowONLY.gif -------------------------------------------------------------------------------- /client/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/headshots/cam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/headshots/cam.png -------------------------------------------------------------------------------- /client/src/assets/headshots/hugh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/headshots/hugh.png -------------------------------------------------------------------------------- /client/src/assets/headshots/john.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/headshots/john.png -------------------------------------------------------------------------------- /client/src/assets/headshots/kola.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/headshots/kola.png -------------------------------------------------------------------------------- /client/src/assets/headshots/nick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/headshots/nick.png -------------------------------------------------------------------------------- /client/src/assets/hughcircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/hughcircle.png -------------------------------------------------------------------------------- /client/src/assets/k8sicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/k8sicon.png -------------------------------------------------------------------------------- /client/src/assets/password-visible-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/password-visible-1.png -------------------------------------------------------------------------------- /client/src/assets/populatedNodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/assets/populatedNodes.png -------------------------------------------------------------------------------- /client/src/components/Chart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | 4 | const Chart = () => { 5 | const [chartData, setChartData] = useState([]); 6 | const [selectedNode, setSelectedNode] = useState(null); 7 | 8 | // { 9 | // name: , 10 | // uid: , 11 | // creationTimestamp: , 12 | // capacity: { 13 | // cpuCapacity: , 14 | // memoryCapacity: , 15 | // podsCapacity: , 16 | // }, 17 | // conditions: [{ 18 | // type: 'FilesystemCorruptionProblem', 19 | // status: 'False', 20 | // lastHeartbeatTime: '2024-03-14T20:28:03Z', 21 | // lastTransitionTime: '2024-03-13T19:03:37Z', 22 | // reason: 'FilesystemIsOK', 23 | // message: 'Filesystem is healthy' 24 | // }], 25 | // totalImages: , 26 | // } 27 | 28 | useEffect(() => { 29 | const fetchData = async () => { 30 | const response = await fetch('/getData/api'); // Fetches from your defined route 31 | const data = await response.json(); 32 | setChartData(data); 33 | }; 34 | 35 | fetchData(); 36 | }, []); 37 | 38 | console.log(chartData) 39 | 40 | const handleNodeChange = (event) => { 41 | setSelectedNode(chartData.filter((el) => el.name === event.target.value)[0]); 42 | }; 43 | console.log("node", selectedNode) 44 | 45 | 46 | 47 | 48 | return ( 49 |
50 |
51 | {/* Dropdown */} 52 | 63 | 64 | {/* Node Information Display */} 65 | {selectedNode && ( 66 |
67 |
68 |

{selectedNode.name}

69 |
    70 |
  • UID: {selectedNode.uid}
  • 71 |
  • Creation Timestamp: {selectedNode.creationTimestamp}
  • 72 |
  • Total Images: {selectedNode.totalImages}
  • 73 | {/* ... Display other properties */} 74 |
75 |
76 |
77 |

Resource Capacity

78 |
    79 |
  • Memory: {selectedNode.capacity.memoryCapacity}
  • 80 |
  • CPU: {selectedNode.capacity.cpuCapacity}
  • 81 |
  • Pods: {selectedNode.capacity.podsCapacity}
  • 82 |
83 |
84 |
85 |

Resource Available

86 |
    87 |
  • Memory: {selectedNode.allocatable.memoryAvailable}
  • 88 |
  • CPU: {selectedNode.allocatable.cpuAvailable}
  • 89 |
  • Pods: {selectedNode.allocatable.podsAvailable}
  • 90 |
91 |
92 |
93 | )} 94 |
95 |
96 | {selectedNode &&
97 | {selectedNode.conditions.map((condition) => ( 98 | 99 | ))} 100 |
} 101 |
102 |
103 | ); 104 | }; 105 | 106 | const ConditionCard = ({ condition}) => { 107 | const getHealthColor = (conditionStatus) => { 108 | // Adjust this to your specific color scheme 109 | if (condition.type === 'Ready') { 110 | return '#51B673' 111 | } 112 | switch (conditionStatus.toLowerCase()) { 113 | case 'true': return '#ee394a'; 114 | case 'false': return '#51B673'; 115 | default: return 'gray'; // Unknown status 116 | } 117 | } 118 | const setActive = (event) => { 119 | event.target.classList.toggle('active') 120 | } 121 | return ( 122 |
123 |
124 | {condition.type} 125 |
126 | 133 |
134 | ); 135 | }; 136 | 137 | export default Chart; 138 | -------------------------------------------------------------------------------- /client/src/components/Cost.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | 4 | 5 | const Cost = () => { 6 | 7 | 8 | return ( 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default Cost; -------------------------------------------------------------------------------- /client/src/components/NodeMap.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as d3 from "d3"; 4 | import { setData, setSidebarData } from "../redux/slices/nodeSlice.js"; 5 | import image from "../assets/k8sicon.png"; 6 | 7 | function NodeMap() { 8 | const dispatch = useDispatch(); 9 | const nodeData = useSelector((state) => state.node.clusterName); 10 | 11 | const data = useSelector((state) => state.node.nodes); 12 | const sidebarData = useSelector((state) => state.node.sidebarData); 13 | //const [nodePositions, setNodePositions] = useState(nodes.map(node => ({ x: node.x, y: node.y }))); 14 | //shape of data: 15 | // const nodeData = { 16 | // name: 'aks-agentpool-29810547-vmss000002', 17 | // cpuCores: '161m', 18 | // memBytes: '1439Mi', 19 | // cpuPercentage: '8%', 20 | // memPercentage: '31%', 21 | // color: 'rgb(144, 238, 144)', 22 | // pods: [ 'prometheus-prometheus-node-exporter-77b64' ] 23 | // }; 24 | // const nodeData = { 25 | // name: resultArray[0], 26 | // cpuCores: resultArray[1], 27 | // memBytes: resultArray[3], 28 | // cpuPercentage: resultArray[2], 29 | // memPercentage: resultArray[4], 30 | // color: color, 31 | // pods: ["name", "name"] 32 | // }; 33 | 34 | //helper function takes in node name that was clicked 35 | const setSidebar = (name) => { 36 | if (name === sidebarData.name || name === "Master Node") { 37 | dispatch(setSidebarData({})); 38 | } else { 39 | const sbData = data.find((el) => el.name === name); 40 | console.log("pre-dispatch sbData", sbData); 41 | dispatch(setSidebarData(sbData)); 42 | } 43 | }; 44 | 45 | useEffect(() => { 46 | setInterval(() => { 47 | console.log("firing fetch in setTimeout"); 48 | 49 | fetch(`http://localhost:3000/getData`) 50 | .then((data) => data.json()) 51 | .then((data) => { 52 | console.log("this is the data I need: ", data); 53 | dispatch(setData(data)); 54 | }) 55 | .catch((err) => { 56 | console.log(err); 57 | }); 58 | }, 5000); 59 | }, [nodeData]); 60 | 61 | //creates a link to the DOM 62 | const svgRef = useRef(); 63 | 64 | //grab the updated k8s data from state 65 | const stateData = useSelector((state) => state.node); 66 | 67 | //iterating through stateData to define nodes and links 68 | const nodes = stateData.nodes.map((node, ix) => ({ 69 | id: ix + 1, 70 | name: node.name, 71 | color: node.color, 72 | })); 73 | console.log(nodes); 74 | nodes.unshift({ id: 0, name: "Master Node", color: "lightgrey" }); 75 | 76 | const links = nodes 77 | .slice(1) 78 | .map((node) => ({ source: nodes[0], target: node.id })); 79 | 80 | const width = 350; 81 | const height = 200; 82 | 83 | useEffect(() => { 84 | d3.select(svgRef.current).selectAll("*").remove(); 85 | 86 | const svg = d3 87 | .select(svgRef.current) 88 | .attr("height", height) 89 | .attr("width", width); 90 | 91 | const group = svg 92 | .append("g") 93 | .attr("transform", "translate(" + width + "," + 500 + ")rotate(-70)"); 94 | 95 | const simulation = d3 96 | .forceSimulation(nodes) 97 | .force( 98 | "link", 99 | d3 100 | .forceLink(links) 101 | .id((d) => d.id) 102 | .distance(300) 103 | ) 104 | .force("charge", d3.forceManyBody().strength(-200)) 105 | .force("center", d3.forceCenter(height, width / 2)) 106 | .on("tick", ticked); 107 | 108 | const link = group 109 | .selectAll(".link") 110 | .data(links) 111 | .enter() 112 | .append("line") 113 | .attr("class", "link") 114 | .style("stroke", "black") 115 | .attr("opacity", 1); 116 | 117 | //changes the radius of nodes depending on number of nodes rendered 118 | const scale = Math.min(70, 490 / nodes.length); 119 | const maxFontSize = scale * 0.6; 120 | // Create nodes 121 | const node = group 122 | .selectAll(".node") 123 | .data(nodes) 124 | .enter() 125 | .append("circle") 126 | .attr("class", "node") 127 | .attr("r", scale) 128 | .style("stroke", "black") 129 | .attr("fill", (d) => d.color) 130 | .on("click", function (event, d) { 131 | //call helper function 132 | //d.id should be a string node name 133 | setSidebar(d.name); 134 | // console.log("nodewas clicked", d.name) 135 | }); 136 | const label = group 137 | .selectAll(".label") 138 | .data(nodes) 139 | .enter() 140 | .append("text") 141 | .attr("class", "label") 142 | .attr("dy", 4) 143 | .attr("text-anchor", "middle") 144 | // .on("mouseover", function (d) { 145 | // d3.select(this).style("opacity", 1); 146 | // }) 147 | // .on("mouseout", function (d) { 148 | // d3.select(this).style("opacity", 0); 149 | // }) 150 | .style("opacity", 1) 151 | .style("font-family", "Play, sans-serif") 152 | .style("font-size", 15) 153 | .text((d) => d.name); 154 | 155 | function getColorBasedOnNodeId(nodeId) { 156 | // Add your logic to determine the color based on the node ID 157 | // For example, you can use a switch statement or an if-else block 158 | // Return the color for the specified node ID 159 | switch (nodeId) { 160 | case 3: 161 | return "yellow"; 162 | case 9: 163 | return "yellow"; 164 | case 6: 165 | return "yellow"; 166 | case 5: 167 | return "red"; 168 | case 0: 169 | return "gray"; 170 | // Add more cases as needed 171 | default: 172 | return "green"; 173 | } 174 | } 175 | 176 | function ticked() { 177 | link 178 | .attr("x1", (d) => d.source.x) 179 | .attr("y1", (d) => d.source.y) 180 | .attr("x2", (d) => d.target.x) 181 | .attr("y2", (d) => d.target.y); 182 | 183 | node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); 184 | 185 | label.attr("x", (d) => d.x).attr("y", (d) => d.y); 186 | label.attr("transform", (d) => `rotate(70, ${d.x}, ${d.y})`); 187 | } 188 | }, []); //positions 189 | 190 | // nodes, links, dispatch 191 | 192 | return ( 193 |
194 | 195 |
196 | ); 197 | } 198 | 199 | export default NodeMap; 200 | -------------------------------------------------------------------------------- /client/src/components/Prompt.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import ConnectCluster from "./prompt/ConnnectCluster.jsx"; 4 | import KubectlInst from "./prompt/KubectlInst.jsx"; 5 | 6 | function Prompt() { 7 | const kubectl = useSelector((state) => { 8 | return state.user.kubectl; 9 | }); 10 | //prompt with connect cluster button 11 | 12 | //kubectl install info (will have a button to reset kubectl to true) 13 | //getData button will send us to getData route 14 | 15 | return
{kubectl ? : }
; 16 | } 17 | 18 | export default Prompt; 19 | -------------------------------------------------------------------------------- /client/src/components/Sidebarsection.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | function Sidebarsection() { 5 | const data = useSelector((state) => state.node.sidebarData); 6 | const name = useSelector((state) => state.node.clusterName); 7 | 8 | let nodeName; 9 | data.name ? (nodeName = data.name.split('-')) : null; 10 | 11 | return ( 12 |
13 |
14 |
Cluster Name:
15 |
{name ? name : null}
{' '} 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 |
Node Name: 24 | {nodeName ? nodeName[3] : null} 25 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 |
CPU: 38 | {' '} 39 | {data ? data.cpuPercentage : null} 40 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 |
CPU Cores: 52 | {data ? data.cpuCores : null} 53 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 |
Memory: 67 | {data ? data.memPercentage : null} 68 |
72 |
73 |
74 | 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 |
Memory Bytes: 80 | {data ? data.memBytes : null} 81 |
85 |
86 |
87 |
88 |
Pods:
89 |
90 |
    91 | {data.pods 92 | ? data.pods.map((pod, index) =>
  • {pod}
  • ) 93 | : null} 94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | 101 | export default Sidebarsection; 102 | -------------------------------------------------------------------------------- /client/src/components/prompt/AwsCLIInst.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const AwsCLIInst = () => { 4 | return ( 5 |
6 |

AWS CLI not installed, please follow the instructions below:

7 |
    8 |
  1. 9 | Linux: 10 |
    11 |             
    12 |               curl
    13 |               "https://d1vvhvl2y92vvt.cloudfront.net/awscli-exe-linux-x86_64.zip"
    14 |               -o "awscliv2.zip" && unzip awscliv2.zip && sudo ./aws/install
    15 |             
    16 |           
    17 |
  2. 18 |
  3. 19 | macOS: 20 |
    21 |             
    22 |               curl "https://d1vvhvl2y92vvt.cloudfront.net/awscli-exe-macos.zip"
    23 |               -o "awscliv2.zip" && unzip awscliv2.zip && sudo ./aws/install
    24 |             
    25 |           
    26 |
  4. 27 |
  5. 28 | Windows: 29 |
    30 |             
    31 |               Download the MSI installer from{" "}
    32 |               here and follow the
    33 |               installation wizard.
    34 |             
    35 |           
    36 |
  6. 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default AwsCLIInst; 43 | -------------------------------------------------------------------------------- /client/src/components/prompt/AwsForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import AwsCLIInst from "./AwsCLIInst.jsx"; 4 | import { aws } from "../../redux/slices/userSlice.js"; 5 | import passwordImage from "../../assets/password-visible-1.png"; 6 | 7 | const AwsForm = () => { 8 | const dispatch = useDispatch(); 9 | const [IDVisible, setIDVisible] = useState(false); 10 | const [KeyVisible, setKeyVisible] = useState(false); 11 | const [chooseForm, setForm] = useState(true); 12 | const [Install, setInstall] = useState(false); 13 | const [message, setMessage] = useState(null); 14 | const [auth, setAuth] = useState(false); 15 | 16 | const awsSubmit = (e) => { 17 | e.preventDefault(); 18 | const data = { 19 | profileName: e.target.elements.profileName.value, 20 | region: e.target.elements.region.value, 21 | accessID: e.target.elements.accessID.value, 22 | accessKey: e.target.elements.accessKey.value, 23 | }; 24 | console.log("this is form submit data:", data); 25 | fetch("http://localhost:3000/awslogin", { 26 | method: "POST", 27 | headers: { "Content-Type": "application/json" }, 28 | body: JSON.stringify(data), 29 | }).then((response) => { 30 | console.log(response); 31 | if (response.status === 200) { 32 | setForm(false); 33 | setAuth(true); 34 | setMessage(null); 35 | } else if (response.status === 404) { 36 | console.log("done"); 37 | setForm(false); 38 | setInstall(true); 39 | setMessage(null); 40 | console.log(Install); 41 | } else 42 | setMessage("Configure failed: Please verify form inputs and try again"); 43 | }); 44 | }; 45 | 46 | const awsAuth = (e) => { 47 | e.preventDefault(); 48 | const data = { 49 | clusterName: e.target.elements.clusterName.value, 50 | }; 51 | console.log("this is form submit data:", data); 52 | fetch("http://localhost:3000/awslogin/auth", { 53 | method: "POST", 54 | headers: { "Content-Type": "application/json" }, 55 | body: JSON.stringify(data), 56 | }).then((response) => { 57 | console.log("hi"); 58 | // dispatch(aws()); 59 | console.log(response); 60 | if (response.status === 200) { 61 | console.log("auth hits frontend"); 62 | dispatch(aws()); 63 | } else { 64 | } 65 | }); 66 | }; 67 | 68 | const togglePasswordVisibility = (name) => { 69 | const inputField = document.getElementsByName(name)[0]; 70 | 71 | if (name === "accessID") { 72 | // Toggle the password visibility 73 | inputField.type = IDVisible ? "password" : "text"; 74 | 75 | // Update the state to reflect the new visibility status 76 | setIDVisible(!IDVisible); 77 | } else { 78 | // Toggle the password visibility 79 | inputField.type = KeyVisible ? "password" : "text"; 80 | 81 | // Update the state to reflect the new visibility status 82 | setKeyVisible(!KeyVisible); 83 | } 84 | }; 85 | 86 | return ( 87 |
88 | {Install ? : null} 89 | {Install ? ( 90 | 97 | ) : null} 98 | {auth ? ( 99 |
100 | 101 | 102 | 105 |
106 | ) : null} 107 | {chooseForm ? ( 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 120 | 137 | 138 | 139 | 140 | 145 | 162 | 163 | 166 |
167 | ) : null} 168 | {

{message}

} 169 |
170 | ); 171 | }; 172 | 173 | export default AwsForm; 174 | -------------------------------------------------------------------------------- /client/src/components/prompt/AzCLIInst.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { aksCLIInfo } from '../../redux/slices/userSlice.js' 4 | 5 | const AzCLIInst = () => { 6 | const dispatch = useDispatch() 7 | 8 | return ( 9 |
10 |

Install Azure CLI:

11 |
    12 |
  1. 13 | Linux: 14 |
    15 |             
    16 |               curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    17 |             
    18 |           
    19 |
  2. 20 |
  3. 21 | macOS: 22 |
    23 |             
    24 |               brew update && brew install azure-cli
    25 |             
    26 |           
    27 |
  4. 28 |
  5. 29 | Windows: 30 |
    31 |             
    32 |               Download the MSI installer from{' '}
    33 |               here and follow the installation wizard.
    34 |             
    35 |           
    36 |
  6. 37 |
38 | 39 |
40 | ); 41 | }; 42 | 43 | export default AzCLIInst; -------------------------------------------------------------------------------- /client/src/components/prompt/ConnnectCluster.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { setData } from "../../redux/slices/nodeSlice.js"; 4 | import { 5 | kubectlSet, 6 | showPrompt, 7 | cloudInfo, 8 | localInfo, 9 | aksForm, 10 | aksInput, 11 | aksCLIInfo, 12 | aws, 13 | } from "../../redux/slices/userSlice.js"; 14 | import LocalInst from "./LocalInst.jsx"; 15 | import AzCLIInst from "./AzCLIInst.jsx"; 16 | import AwsForm from "./AwsForm.jsx"; 17 | 18 | function ConnectCluster() { 19 | const dispatch = useDispatch(); 20 | const show = useSelector((state) => state.user.showPrompt); 21 | 22 | //Booleans from state to conditionally render elements in the return statement 23 | const cloud = useSelector((state) => state.user.cloudInfo); 24 | const local = useSelector((state) => state.user.localInfo); 25 | const aks = useSelector((state) => state.user.aksForm); 26 | const result = useSelector((state) => state.user.aksResult); 27 | const aksCLI = useSelector((state) => state.user.aksCLI); 28 | const awsForm = useSelector((state) => state.user.awsForm); 29 | 30 | const getData = () => { 31 | fetch(`http://localhost:3000/getData`) 32 | .then((data) => data.json()) 33 | .then((data) => { 34 | console.log("this is the data I need: ", data); 35 | if (data === false) { 36 | dispatch(kubectlSet()); 37 | } else if (data.clusterName === "") { 38 | dispatch(showPrompt()); 39 | } else { 40 | console.log("correctly sending back data"); 41 | dispatch(setData(data)); 42 | } 43 | }) 44 | .catch((err) => { 45 | console.log(err); 46 | }); 47 | }; 48 | 49 | const AKSfetch = () => { 50 | fetch("http://localhost:3000/azlogin") 51 | .then((response) => { 52 | console.log("this is response in AKS FETCH:", response.status); 53 | if (response.status === 200) { 54 | console.log("we did it!!!!"); 55 | dispatch(cloudInfo()); 56 | dispatch(aksForm()); 57 | } else if (response.status === 404) { 58 | dispatch(aksCLIInfo()); 59 | } 60 | }) 61 | .catch((err) => { 62 | console.log(`This is error in AKS fetch: ${err}`); 63 | }); 64 | }; 65 | 66 | //Handles submitting of aks info form 67 | const aksSubmit = (e) => { 68 | e.preventDefault(); 69 | const data = { 70 | clusterName: e.target.elements.clusterName.value, 71 | resourceGroup: e.target.elements.resourceGroup.value, 72 | }; 73 | console.log("this is form submit data:", data); 74 | fetch("http://localhost:3000/azlogin", { 75 | method: "POST", 76 | headers: { "Content-Type": "application/json" }, 77 | body: JSON.stringify(data), 78 | }).then((response) => { 79 | console.log(response); 80 | if (response.status === 200) { 81 | dispatch(aksInput(true)); 82 | } else dispatch(aksInput(false)); 83 | }); 84 | }; 85 | 86 | return ( 87 |
88 | {show ? ( 89 | 90 | 100 | 110 | 111 | ) : null} 112 | 113 | {cloud ? ( 114 |
115 |
116 | {aksCLI ? : null} 117 | 127 |
128 |
129 | {/* {awsCLI ? : null} */} 130 | 131 | 142 |
143 |
144 | ) : null} 145 | {awsForm ? : null} 146 | {local ? ( 147 |
148 | 149 |
150 | ) : null} 151 | 152 | {aks ? ( 153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 |
161 | {result === true ? ( 162 |
163 | Successfully added a Kube config file, please try to Render Node 164 | Map! 165 | 175 |
176 | ) : null} 177 | {result === false ? ( 178 |
Failed to add a Kube config file, please try again!
179 | ) : null} 180 |
181 | ) : null} 182 | 183 | {aksCLI || aks || show || local || cloud || awsForm ? null : ( 184 | 191 | )} 192 |
193 | ); 194 | } 195 | 196 | export default ConnectCluster; 197 | -------------------------------------------------------------------------------- /client/src/components/prompt/KubectlInst.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {useDispatch} from "react-redux" 3 | import {kubectlSet} from "../../redux/slices/userSlice.js" 4 | 5 | const KubectlInst = () => { 6 | const dispatch = useDispatch() 7 | return ( 8 |
9 |

Install kubectl:

10 |

11 | To begin, install the Kubernetes command-line tool, kubectl, using one of the following methods: 12 |

13 |
14 |         
15 |         Linux:
16 | sudo apt-get update && sudo apt-get install -y kubectl

17 | 18 | MacOS:
19 | brew install kubectl

20 | 21 | Windows:
22 | Download the kubectl executable from here and add it to your system's PATH.
23 | Once you've installed kubectl, please click Reconnect Cluster. 24 |
25 |
26 | 29 |
30 | ); 31 | }; 32 | 33 | export default KubectlInst; -------------------------------------------------------------------------------- /client/src/components/prompt/LocalInst.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { localInfo } from "../../redux/slices/userSlice.js"; 4 | 5 | const LocalInst = () => { 6 | const dispatch = useDispatch(); 7 | return ( 8 |
9 |

Set Up Local Kubernetes Cluster and kubeconfig File:

10 |

11 | Follow the steps below to set up a local Kubernetes cluster and 12 | configure the kubeconfig file: 13 |

14 |
    15 |
  1. 16 | Install a Local Kubernetes Cluster: 17 |
    {/* Instructions for installing a local cluster */}
    18 |
  2. 19 |
  3. 20 | Start the Local Cluster: 21 |
    {/* Instructions for starting the local cluster */}
    22 |
  4. 23 |
  5. 24 | Configure kubectl: 25 |
    26 |             
    27 |               {/* Set kubectl context */}
    28 |               kubectl config set-context <context-name>
    29 |               --cluster=<cluster-name> --user=<user-name> kubectl
    30 |               config use-context <context-name>
    31 |             
    32 |           
    33 |
  6. 34 |
35 |

36 | Adjust the context name, cluster name, and user name according to your 37 | local cluster configuration. These instructions assume you've already 38 | installed kubectl. 39 |

40 | 48 |
49 | ); 50 | }; 51 | 52 | export default LocalInst; 53 | -------------------------------------------------------------------------------- /client/src/containers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/containers/.DS_Store -------------------------------------------------------------------------------- /client/src/containers/AsclepiusLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/client/src/containers/AsclepiusLogo.png -------------------------------------------------------------------------------- /client/src/containers/HeaderContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { setPage } from "../redux/slices/userSlice"; 4 | import AsclepiusLogo from "./AsclepiusLogo.png"; 5 | 6 | 7 | function HeaderContainer() { 8 | const dispatch = useDispatch(); 9 | return ( 10 |
11 |
12 | 13 |
Asclepius
14 |
15 | 22 | 29 | 36 |
37 | ); 38 | } 39 | 40 | export default HeaderContainer; 41 | -------------------------------------------------------------------------------- /client/src/containers/NodeMapContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NodeMap from "../components/NodeMap.jsx"; 3 | import Prompt from "../components/Prompt.jsx"; 4 | import { useSelector } from "react-redux"; 5 | import SideBarContainer from "./SideBarContainer.jsx"; 6 | import Chart from "../components/Chart.jsx"; 7 | import Cost from "../components/Cost.jsx" 8 | 9 | 10 | function NodeMapContainer() { 11 | //useSelector to listen for boolean state (do we have data?) 12 | const stateData = useSelector((state) => { 13 | return state.node.clusterName; 14 | }); 15 | console.log(stateData); 16 | const page = useSelector((state) => state.user.page); 17 | 18 | 19 | return ( 20 |
21 | {stateData === "" ? ( 22 | 23 | ) : ( 24 |
25 | {page === "map" ? : null} 26 | {page === "map" ? : null} 27 | {page === "charts" ? : null} 28 | {page === "cost" ? : null} 29 |
30 | )} 31 |
32 | ); 33 | } 34 | 35 | export default NodeMapContainer; 36 | -------------------------------------------------------------------------------- /client/src/containers/SideBarContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Sidebarsection from "../components/Sidebarsection.jsx"; 3 | import arrow from "../assets/arrow.png" 4 | 5 | function SideBarContainer() { 6 | 7 | const handleToggle = () => { 8 | const sidebar = document.querySelector('.sidebar') 9 | sidebar.classList.toggle('active') 10 | } 11 | //id="SideBarContainer" 12 | return ( 13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default SideBarContainer; 21 | -------------------------------------------------------------------------------- /client/src/redux/slices/nodeSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const nodeSlice = createSlice({ 4 | name: "nodeName", 5 | initialState: { 6 | clusterName: "", 7 | nodes: [], 8 | // clusterName: "demoAKS", 9 | // nodes: [{ 10 | // name: 'Node 1', 11 | // cpuCores: '305m', 12 | // memBytes: '3885Mi', 13 | // cpuPercentage: '16%', 14 | // memPercentage: '85%', 15 | // color: 'red', 16 | // pods: ['pod_1','pod_2','pod_3',] 17 | // }], 18 | // nodes: [], 19 | sidebarData: {}, 20 | }, 21 | 22 | reducers: { 23 | setData: (state, action) => { 24 | return { 25 | ...state, 26 | clusterName: action.payload.clusterName, 27 | nodes: action.payload.nodes, 28 | }; 29 | }, 30 | setSidebarData: (state, action) => { 31 | state.sidebarData = action.payload; 32 | }, 33 | }, 34 | }); 35 | 36 | export const { setData, setSidebarData, setChart } = nodeSlice.actions; 37 | 38 | export default nodeSlice.reducer; 39 | -------------------------------------------------------------------------------- /client/src/redux/slices/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const userSlice = createSlice({ 4 | name: "user", 5 | //intial states should be what? 6 | initialState: { 7 | page: "map", 8 | kubectl: true, 9 | showPrompt: false, 10 | cloudInfo: false, 11 | localInfo: false, 12 | aksForm: false, 13 | aksResult: null, 14 | aksCLI: false, 15 | awsForm: false, 16 | }, 17 | 18 | reducers: { 19 | setPage: (state, action) => { 20 | state.page = action.payload; 21 | }, 22 | kubectlSet: (state) => { 23 | const newState = { ...state }; 24 | newState.kubectl = !newState.kubectl; 25 | 26 | return newState; 27 | }, 28 | showPrompt: (state) => { 29 | if (state.showPrompt === true) { 30 | state.showPrompt = false; 31 | } else state.showPrompt = true; 32 | }, 33 | cloudInfo: (state) => { 34 | if (state.cloudInfo === true) { 35 | state.cloudInfo = false; 36 | } else state.cloudInfo = true; 37 | }, 38 | localInfo: (state) => { 39 | if (state.localInfo === true) { 40 | state.localInfo = false; 41 | } else state.localInfo = true; 42 | }, 43 | aksForm: (state) => { 44 | if (state.aksForm === true) { 45 | state.aksForm = false; 46 | } else state.aksForm = true; 47 | }, 48 | aksInput: (state, action) => { 49 | console.log(action.payload); 50 | state.aksResult = action.payload; 51 | }, 52 | aksCLIInfo: (state) => { 53 | if (state.aksCLI === true) { 54 | state.aksCLI = false; 55 | } else state.aksCLI = true; 56 | }, 57 | aws: (state) => { 58 | if (state.awsForm === true) { 59 | state.awsForm = false; 60 | } else state.awsForm = true; 61 | }, 62 | }, 63 | }); 64 | 65 | export const { 66 | setPage, 67 | kubectlSet, 68 | showPrompt, 69 | cloudInfo, 70 | localInfo, 71 | aksForm, 72 | aksInput, 73 | aksCLIInfo, 74 | aws, 75 | } = userSlice.actions; 76 | 77 | export default userSlice.reducer; 78 | -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import nodeSlice from "./slices/nodeSlice.js"; 3 | import userSlice from "./slices/userSlice.js"; 4 | 5 | export const store = configureStore({ 6 | reducer: { 7 | node: nodeSlice, 8 | user: userSlice, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /client/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@700&family=Play:wght@400;700&display=swap'); 2 | 3 | $primary-100: #7b91c5; 4 | $primary-200: #8a9dcb; 5 | $primary-300: #99a8d2; 6 | $primary-400: #a7b4d8; 7 | $primary-500: #b6c0df; 8 | $primary-600: #c4cde5; 9 | $green: #51B673; 10 | /** SCSS DARK THEME SURFACE COLORS */ 11 | $surface-100: #121212; 12 | $surface-200: #282828; 13 | $surface-300: #3f3f3f; 14 | $surface-400: #575757; 15 | $surface-500: #717171; 16 | $surface-600: #8b8b8b; 17 | /** SCSS DARK THEME MIXED SURFACE COLORS */ 18 | $surface-mixed-100: #1c1d21; 19 | $surface-mixed-200: #313236; 20 | $surface-mixed-300: #47484c; 21 | $surface-mixed-400: #5f5f63; 22 | $surface-mixed-500: #77787b; 23 | $surface-mixed-600: #919194; 24 | 25 | body { 26 | margin: 0; 27 | height: 100%; 28 | width: 100%; 29 | font-family: 'Comfortaa', sans-serif; 30 | background-color: $surface-mixed-400; 31 | } 32 | #AppContainer { 33 | display: grid; 34 | grid-template-columns: auto 1fr 1fr 1fr; 35 | grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 36 | grid-template-areas: 37 | 'header header header header' 38 | 'sidebar main main main' 39 | 'sidebar main main main' 40 | 'sidebar main main main' 41 | 'sidebar main main main' 42 | 'sidebar main main main' 43 | 'sidebar main main main' 44 | 'sidebar main main main'; 45 | height: 100%; 46 | width: 100%; 47 | } 48 | 49 | #HeaderContainer { 50 | grid-area: header; 51 | display: grid; 52 | grid-template-columns: 1fr 1fr 1fr 1fr; 53 | grid-template-rows: 1fr; 54 | grid-template-areas: 'logo map charts cost'; 55 | background: $surface-mixed-400; 56 | #logocontainer { 57 | grid-area: logo; 58 | margin: 5px; 59 | padding-left: 10px; 60 | padding-right: 10px; 61 | display: flex; 62 | justify-content: start; 63 | align-items: center; 64 | 65 | #asclepiusLogo { 66 | width: 4rem; 67 | height: 4rem; 68 | background-color: transparent; 69 | border: 2px solid $primary-300; 70 | border-radius: 20px; 71 | padding: 5px; 72 | } 73 | #asclepiusText { 74 | font-family: 'Comfortaa', sans-serif; 75 | padding-top: 10px; 76 | padding-left: 10px; 77 | font-size: 2.2rem; 78 | font-weight: 700; 79 | color: white; 80 | } 81 | } 82 | #map { 83 | grid-area: map; 84 | background: $primary-100; 85 | border-radius: 10px; 86 | } 87 | #charts { 88 | grid-area: charts; 89 | background: $primary-100; 90 | border-radius: 10px; 91 | } 92 | #cost { 93 | grid-area: cost; 94 | background: $primary-100; 95 | border-radius: 10px; 96 | } 97 | } 98 | 99 | 100 | .sidebar { 101 | height: 100%; 102 | width: 20%; 103 | display: grid; 104 | grid-area: sidebar; 105 | grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 106 | grid-template-columns: 5fr 1fr; 107 | background-color: $surface-mixed-400; 108 | align-items: start; 109 | justify-items: center; 110 | grid-template-areas: 111 | 'sidebar move' 112 | 'sidebar move' 113 | 'sidebar move' 114 | 'sidebar move' 115 | 'sidebar move' 116 | 'sidebar move' 117 | 'sidebar move'; 118 | transition: transform 300ms ease-in; 119 | .arrow { 120 | grid-area: move; 121 | display: flex; 122 | justify-self: center; 123 | align-self: center; 124 | transform: rotate(0deg); 125 | transition: transform 300ms ease; 126 | 127 | #img { 128 | height: 30px; 129 | } 130 | } 131 | .arrow:hover { 132 | cursor: pointer; 133 | } 134 | 135 | #info { 136 | grid-area: sidebar; 137 | height: 100%; 138 | width: 100%; 139 | display: flex; 140 | flex-direction: column; 141 | gap: 10px; 142 | margin-top: 15px; 143 | margin-left: 10px; 144 | #cluster { 145 | color: white; 146 | font-size: larger; 147 | display: flex; 148 | flex-direction: column; 149 | gap: 5px; 150 | align-items: center; 151 | justify-content: center; 152 | padding: 5px; 153 | border-radius: 20px; 154 | background-color: $primary-100; 155 | border: 2px solid $surface-mixed-500; 156 | box-shadow: $surface-mixed-300 0px 4px; 157 | } 158 | .connectedDataText { 159 | color: black; 160 | } 161 | 162 | #small { 163 | display: flex; 164 | flex-direction: row; 165 | justify-content: space-evenly; 166 | align-items: center; 167 | background: #dddddd; 168 | margin: 5px; 169 | padding: 5px; 170 | border-radius: 30px; 171 | border: 2px solid $surface-mixed-500; 172 | box-shadow: $surface-mixed-300 0px 4px; 173 | } 174 | #cpu { 175 | display: flex; 176 | flex-direction: column; 177 | justify-content: space-evenly; 178 | align-items: center; 179 | background: #dddddd; 180 | margin: 5px; 181 | padding: 5px; 182 | border: 2px solid $surface-mixed-500; 183 | border-radius: 30px; 184 | box-shadow: $surface-mixed-300 0px 4px; 185 | } 186 | #mem { 187 | display: flex; 188 | flex-direction: column; 189 | align-items: center; 190 | padding: 5px; 191 | padding-left: 25px; 192 | background: #dddddd; 193 | margin: 5px; 194 | border: 2px solid $surface-mixed-500; 195 | border-radius: 30px; 196 | box-shadow: $surface-mixed-300 0px 4px; 197 | } 198 | #big { 199 | background: #dddddd; 200 | display: flex; 201 | flex-direction: column; 202 | align-items: center; 203 | justify-content: space-evenly; 204 | margin: 5px; 205 | padding: 5px; 206 | border: 2px solid $surface-mixed-500; 207 | border-radius: 30px; 208 | box-shadow: $surface-mixed-300 0px 4px; 209 | } 210 | } 211 | 212 | } 213 | .sidebar.active { 214 | height: 100%; 215 | display: grid; 216 | transition: transform 300ms ease-out; 217 | grid-area: sidebar; 218 | grid-template-rows: 1fr; 219 | grid-template-columns: 5fr 1fr; 220 | align-items: start; 221 | justify-items: center; 222 | grid-template-areas:'sidebar move'; 223 | transform: translateX(-85%); 224 | 225 | .arrow { 226 | grid-area: move; 227 | display: flex; 228 | justify-self: center; 229 | align-self: center; 230 | #img { 231 | height: 30px; 232 | transform: rotate(-180deg); 233 | transition: transform 300ms ease; 234 | } 235 | } 236 | .arrow:hover { 237 | cursor: pointer; 238 | } 239 | 240 | #info { 241 | grid-area: sidebar; 242 | width: 100%; 243 | height: 100%; 244 | display: flex; 245 | flex-direction: column; 246 | gap: 10px; 247 | margin-top: 15px; 248 | margin-left: 10px; 249 | #cluster { 250 | color: $surface-mixed-100; 251 | font-size: larger; 252 | display: flex; 253 | flex-direction: column; 254 | gap: 5px; 255 | align-items: center; 256 | justify-content: center; 257 | padding: 5px; 258 | border-radius: 20px; 259 | background-color: $primary-100; 260 | border: 2px solid $surface-mixed-500; 261 | box-shadow: $surface-mixed-300 0px 4px; 262 | #name { 263 | color: white; 264 | } 265 | } 266 | .connectedDataText { 267 | color: black; 268 | } 269 | 270 | #small { 271 | color: black; 272 | display: flex; 273 | flex-direction: row; 274 | justify-content: space-evenly; 275 | align-items: center; 276 | background: #dddddd; 277 | margin: 5px; 278 | padding: 5px; 279 | border-radius: 30px; 280 | border: 2px solid $surface-mixed-500; 281 | box-shadow: $surface-mixed-300 0px 4px; 282 | } 283 | #cpu { 284 | display: flex; 285 | flex-direction: column; 286 | justify-content: space-evenly; 287 | align-items: center; 288 | background: #dddddd; 289 | color: black; 290 | margin: 5px; 291 | padding: 5px; 292 | border: 2px solid $surface-mixed-500; 293 | border-radius: 30px; 294 | box-shadow: $surface-mixed-300 0px 4px; 295 | } 296 | #mem { 297 | display: flex; 298 | color: black; 299 | flex-direction: column; 300 | align-items: center; 301 | padding: 5px; 302 | padding-left: 25px; 303 | background: #dddddd; 304 | margin: 5px; 305 | border: 2px solid $surface-mixed-500; 306 | border-radius: 30px; 307 | box-shadow: $surface-mixed-300 0px 4px; 308 | } 309 | #big { 310 | background: #dddddd; 311 | color: black; 312 | display: flex; 313 | flex-direction: column; 314 | align-items: center; 315 | justify-content: space-evenly; 316 | margin: 5px; 317 | padding: 5px; 318 | border: 2px solid $surface-mixed-500; 319 | border-radius: 30px; 320 | box-shadow: $surface-mixed-300 0px 4px; 321 | } 322 | } 323 | 324 | } 325 | #NodeMapContainer { 326 | height: 100%; 327 | width: 100%; 328 | color: white; 329 | grid-area: main; 330 | display: flex; 331 | justify-content: center; 332 | align-items: center; 333 | background: #dddddd; 334 | border-top: 10px solid $surface-mixed-300; 335 | #content { 336 | height: 100%; 337 | width: 100%; 338 | display: flex; 339 | flex-direction: row; 340 | #innerNodeMapContainer { 341 | height: 100%; 342 | width: 100%; 343 | border: none; 344 | border-radius: 10px; 345 | justify-self: flex-end; 346 | background-size: cover; 347 | background-repeat: no-repeat; 348 | background-color: #dddddd; 349 | background-position: center; 350 | display: flex; 351 | justify-content: center; 352 | align-items: center; 353 | #NodeMap { 354 | border: 0px; 355 | height: 90%; 356 | width: 90%; 357 | align-content: center; 358 | display: flex; 359 | justify-content: center; 360 | .node { 361 | stroke: $primary-100; 362 | stroke-width: 2px; 363 | } 364 | .link { 365 | stroke: $primary-300; 366 | stroke-width: 2px; 367 | } 368 | } 369 | } 370 | .newMap { 371 | flex: 1; 372 | height: 100%; 373 | width: 100%; 374 | border: none; 375 | border-radius: 10px; 376 | justify-self: flex-end; 377 | background-size: cover; 378 | background-repeat: no-repeat; 379 | background-color: #dddddd; 380 | background-position: center; 381 | display: flex; 382 | justify-content: center; 383 | align-items: center; 384 | border: 0px; 385 | height: 90%; 386 | width: 90%; 387 | align-content: center; 388 | display: flex; 389 | justify-content: center; 390 | } 391 | .cost { 392 | flex: 1; 393 | height: 100%; 394 | width: 100%; 395 | border: none; 396 | border-radius: 10px; 397 | justify-self: flex-end; 398 | background-size: cover; 399 | background-repeat: no-repeat; 400 | background-color: #dddddd; 401 | background-position: center; 402 | display: flex; 403 | justify-content: center; 404 | align-items: center; 405 | border: 0px; 406 | height: 90%; 407 | width: 90%; 408 | align-content: center; 409 | display: flex; 410 | justify-content: center; 411 | } 412 | #underline { 413 | text-decoration: underline; 414 | margin-bottom: 5px; 415 | } 416 | } 417 | 418 | } 419 | 420 | .newButton, 421 | .renderButton { 422 | display: flex; 423 | justify-content: center; 424 | appearance: none; 425 | background-color: $primary-100; 426 | border: 2px solid $surface-mixed-300; 427 | border-radius: 15px; 428 | box-shadow: $surface-mixed-300 0px 4px; 429 | color: white; 430 | cursor: pointer; 431 | display: inline-block; 432 | font-family: Roobert, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 433 | Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 434 | font-size: 16px; 435 | font-weight: 600; 436 | line-height: normal; 437 | margin: 0; 438 | margin-bottom: 20px; 439 | min-height: 60px; 440 | min-width: 0; 441 | outline: none; 442 | padding: 16px 24px; 443 | text-align: center; 444 | text-decoration: none; 445 | transition: all 300ms cubic-bezier(0.23, 1, 0.32, 1); 446 | user-select: none; 447 | -webkit-user-select: none; 448 | touch-action: manipulation; 449 | will-change: transform; 450 | } 451 | .newButton:hover, 452 | .renderButton:hover { 453 | color: #fff; 454 | background-color: $surface-mixed-500; 455 | box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px; 456 | transform: translateY(-3px); 457 | } 458 | 459 | .renderButton { 460 | margin: 20px; 461 | } 462 | 463 | form { 464 | border: 5px solid $primary-100; 465 | border-radius: 10px; 466 | padding: 10px; 467 | color: white; 468 | display: flex; 469 | flex-direction: column; 470 | background-color: $surface-mixed-300; 471 | img { 472 | background-color: $primary-100; 473 | border: 2px solid black; 474 | border-radius: 50%; 475 | } 476 | } 477 | 478 | input[type='text'], 479 | input[type='password'] { 480 | border: 2px solid black; 481 | color: $primary-100; 482 | margin-top: 20px; 483 | margin-bottom: 20px; 484 | margin-left: 70px; 485 | } 486 | 487 | #localButton { 488 | margin-left: 70px; 489 | } 490 | 491 | #buttonSpan { 492 | display: flex; 493 | flex-direction: row; 494 | button { 495 | margin: 5px; 496 | } 497 | } 498 | .chartContainer { 499 | width: 100%; 500 | height: 100%; 501 | display: grid; 502 | margin: 20px; 503 | grid-template-columns: 1fr 1fr; 504 | place-content: center; 505 | place-items: center; 506 | } 507 | .chart { 508 | height: 100%; 509 | width: 100%; 510 | border: none; 511 | gap: 20px; 512 | border-radius: 10px; 513 | display: flex; 514 | flex-direction: column; 515 | place-items: center; 516 | .select { 517 | background-color: #dddddd; 518 | width: 20%; 519 | place-content: center; 520 | border: solid $surface-mixed-200 4px; 521 | border-radius: 20px; 522 | margin: 10px; 523 | padding: 10px; 524 | } 525 | .node-details { 526 | display: grid; 527 | gap: 20px; 528 | margin: 20px; 529 | grid-template-columns: 1fr 1fr; 530 | grid-template-areas: 531 | "name name" 532 | "capacity allocatable" 533 | "conditions-grid conditions-grid"; 534 | color: black; 535 | place-items: start; 536 | } 537 | .name { 538 | grid-area: name; 539 | } 540 | .capacity { 541 | background-color: $surface-mixed-600; 542 | grid-area: capacity; 543 | width: 90%; 544 | border: solid $surface-mixed-200 4px; 545 | border-radius: 30px; 546 | place-items: center; 547 | padding: 5px; 548 | li {padding: 5px;} 549 | } 550 | .allocatable { 551 | background-color: $surface-mixed-600; 552 | 553 | grid-area: allocatable; 554 | width: 90%; 555 | border: solid $surface-mixed-200 4px; 556 | border-radius: 30px; 557 | place-items: center; 558 | padding: 5px; 559 | li {padding: 5px;} 560 | } 561 | 562 | } 563 | .conditions-grid { 564 | width: 100%; 565 | height: 100%; 566 | grid-area: conditions-grid; 567 | display: grid; 568 | grid-template-columns: repeat(3, 1fr); /* Adjust for 3 columns */ 569 | gap: 10px; 570 | grid-auto-flow: dense; 571 | } 572 | 573 | .condition-card { 574 | padding: 10px; 575 | border: 1px solid #ccc; 576 | cursor: pointer; 577 | transition: grid-column-end 0.5s ease-out; 578 | 579 | } 580 | .condition-card.active { 581 | display: flex; 582 | flex-direction: column; 583 | grid-row-end: span 2; 584 | border: solid $surface-mixed-400 4px; 585 | transition: grid-column-end 0.5s ease-out; 586 | } 587 | 588 | .condition-card ul { 589 | max-height: 0; /* Initially hide the details */ 590 | overflow: hidden; 591 | transition: max-height 0.5s ease-out; /* Smooth transition */ 592 | } 593 | 594 | .condition-card.active ul { 595 | max-height: 100%; /* Example height when expanded */ 596 | } -------------------------------------------------------------------------------- /dist/2a3905e4d4786d742b06b9cfba58a3a9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Asclepius/8020b6e639f4e8bd486fa7fdf1c80239c1f68a31/dist/2a3905e4d4786d742b06b9cfba58a3a9.png -------------------------------------------------------------------------------- /dist/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * use-sync-external-store-with-selector.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | Asclepius!
-------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // testEnvironment: 'node', 3 | // forceExit: true, 4 | // } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asclepius", 3 | "version": "1.0.0", 4 | "description": "Kubernetes cluster health monitoring tool", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"nodemon ./server/server.ts\" \"webpack-dev-server --mode development --open --hot\"", 8 | "build": "webpack --mode production", 9 | "test": "jest --verbose" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@babel/plugin-transform-async-to-generator": "^7.23.3", 16 | "@babel/runtime": "^7.23.5", 17 | "@kubernetes/client-node": "^0.20.0", 18 | "@reduxjs/toolkit": "^2.0.1", 19 | "concurrently": "^8.2.2", 20 | "cors": "^2.8.5", 21 | "css-loader": "^6.8.1", 22 | "d3": "^7.8.5", 23 | "express": "^4.18.2", 24 | "node": "^21.2.0", 25 | "nodemon": "^3.0.2", 26 | "os": "^0.1.2", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-redux": "^9.0.4", 30 | "sass-loader": "^14.1.0", 31 | "selenium-webdriver": "^4.16.0", 32 | "style-loader": "^3.3.3", 33 | "supertest": "^6.3.3", 34 | "ts-node": "^10.9.2" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.23.6", 38 | "@babel/plugin-transform-runtime": "^7.1.0", 39 | "@babel/preset-env": "^7.23.6", 40 | "@babel/preset-react": "^7.23.3", 41 | "@testing-library/jest-dom": "^6.1.4", 42 | "@testing-library/react": "^14.1.2", 43 | "@types/express": "^4.17.21", 44 | "@types/node": "^20.11.16", 45 | "babel-jest": "^29.7.0", 46 | "babel-loader": "^9.1.3", 47 | "file-loader": "^6.2.0", 48 | "html-webpack-plugin": "^5.5.4", 49 | "jest": "^29.7.0", 50 | "jest-environment-jsdom": "^29.7.0", 51 | "regenerator-runtime": "^0.14.0", 52 | "sass": "^1.70.0", 53 | "typescript": "^5.3.3", 54 | "webpack": "^5.89.0", 55 | "webpack-cli": "^5.1.4", 56 | "webpack-dev-server": "^4.15.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/controllers/awsController.ts: -------------------------------------------------------------------------------- 1 | const { spawn, spawnSync } = require("child_process"); 2 | const fs = require("fs").promises; 3 | 4 | const awsController = { 5 | isAWSCLIInstalled: (req, res, next) => { 6 | const result = spawnSync("aws", ["--version"], { 7 | encoding: "utf-8", 8 | shell: true, 9 | }); 10 | console.log("aws version: ", result); 11 | if (result.stderr) { 12 | res.locals.awsInstalled = false; 13 | return next(); 14 | } else res.locals.awsInstalled = true; 15 | return next(); 16 | }, 17 | 18 | awsConfigure: async (req, res, next) => { 19 | const { profileName, region, accessID, accessKey } = req.body; 20 | if (res.locals.awsInstalled === false) { 21 | return next(); 22 | } 23 | const awsFolderPath = `${process.env.HOME || process.env.USERPROFILE}/.aws`; 24 | const credentialsPath = `${awsFolderPath}/credentials`; 25 | const configPath = `${awsFolderPath}/config`; 26 | 27 | // AWS credentials content 28 | const credentialsContent = `[default]\naws_access_key_id = ${accessID}\naws_secret_access_key = ${accessKey}`; 29 | 30 | // AWS config content 31 | const configContent = `[default]\nregion = ${region}\noutput = json`; 32 | 33 | try { 34 | // Write credentials file 35 | await fs.writeFile(credentialsPath, credentialsContent); 36 | console.log("Credentials file created successfully."); 37 | 38 | // Write config file 39 | await fs.writeFile(configPath, configContent); 40 | console.log("Config file created successfully."); 41 | const result = spawnSync("kubectl", [ 42 | "get pods -n kube-system | grep metrics-server", 43 | ]); 44 | if (result.stderr) { 45 | res.locals.metrics = false; 46 | } else res.locals.metrics = true; 47 | } catch (error) { 48 | console.error("Error creating AWS config files:", error); 49 | } 50 | 51 | //make 2 config files: credentials and config 52 | //after success, check "kubectl get pods -n kube-system | grep metrics-server" 53 | //if success set boolean metrics to true 54 | //if failure set boolean metrics to false 55 | return next(); 56 | }, 57 | 58 | awsMetrics: (req, res, next) => { 59 | if (res.locals.metrics === true) { 60 | return next(); 61 | } 62 | const result = spawnSync("kubectl", [ 63 | "apply", 64 | "-f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml", 65 | ]); 66 | return next(); 67 | }, 68 | 69 | awsAuthenticate: (req, res, next) => { 70 | const { clusterName } = req.body; 71 | console.log("body", req.body); 72 | // Step 1: Authenticate with AWS 73 | const loginResult = spawnSync("aws", [ 74 | "eks", 75 | "update-kubeconfig", 76 | "--name", 77 | clusterName, 78 | ]); 79 | console.log(loginResult.stdout.toString()); 80 | console.error(loginResult.stderr.toString()); 81 | return next(); 82 | }, 83 | }; 84 | 85 | export = awsController; 86 | -------------------------------------------------------------------------------- /server/controllers/azController.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync } from "child_process"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | interface AzController { 5 | isAzureCliInstalled: ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => void; 10 | azLogin: (req: Request, res: Response, next: NextFunction) => void; 11 | azCredentials: (req: Request, res: Response, next: NextFunction) => void; 12 | } 13 | 14 | const azController = { 15 | isAzureCliInstalled: (req, res, next) => { 16 | const result = spawnSync("az", ["--version"], { 17 | encoding: "utf-8", 18 | shell: true, 19 | }); 20 | console.log(result); 21 | if (result.stderr) { 22 | let resultErr = result.stderr.split(""); 23 | //check if the first letter in output for stderr is capital E for Error 24 | if (resultErr[0] === "W") { 25 | const upgrade = spawnSync("az", ["upgrade", "--yes"], { 26 | encoding: "utf-8", 27 | shell: true, 28 | }); 29 | console.log(upgrade); 30 | if (!upgrade.stderr) { 31 | return next(); 32 | } 33 | } 34 | res.locals.azInstalled = false; 35 | return next(); 36 | } else res.locals.azInstalled = true; 37 | return next(); 38 | }, 39 | 40 | azLogin: (req, res, next) => { 41 | if (res.locals.azInstalled === false) { 42 | return next(); 43 | } 44 | const result = spawnSync("az", ["login"], { 45 | encoding: "utf-8", 46 | shell: true, 47 | }); 48 | console.log("this is result in azLogin middleware", result); 49 | if (result.stderr) { 50 | let stderrresult = result.stderr.split(""); 51 | //check if the first letter in output for stderr is capital E for Error 52 | if (result[0] === "E") { 53 | return next({ 54 | log: `azLogin has caught an error with the result of "az login", ${result.stderr}`, 55 | status: 500, 56 | message: { err: "An error occured" }, 57 | }); 58 | } 59 | } 60 | console.log("right before next() in azLogin"); 61 | return next(); 62 | }, 63 | 64 | azCredentials: (req, res, next) => { 65 | const { clusterName, resourceGroup } = req.body; 66 | console.log("body", req.body); 67 | //az aks get-credentials --name ${name} --resource-group ${resource-group} 68 | const result = spawnSync( 69 | "az", 70 | [ 71 | "aks", 72 | "get-credentials", 73 | `--name ${clusterName}`, 74 | `--resource-group ${resourceGroup}`, 75 | ], 76 | { 77 | encoding: "utf-8", 78 | shell: true, 79 | } 80 | ); 81 | console.log(result); 82 | //result.output[2] should be warning not error 83 | const code = result.output[2].split(" "); 84 | console.log("should be status", code[0]); 85 | if (code[0] === "Error:") { 86 | console.log( 87 | "Error: Cluster name or resource group is incorrect. Please try again." 88 | ); 89 | res.locals.formsuccess = false; 90 | return next(); 91 | } else if (code[0] === "WARNING:") { 92 | console.log("here"); 93 | res.locals.formsuccess = true; 94 | return next(); 95 | } 96 | }, 97 | }; 98 | 99 | export = azController; 100 | -------------------------------------------------------------------------------- /server/controllers/dataController.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync, SpawnSyncReturns } from "child_process"; 2 | import os from "os"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import { Data, NodeData } from "../types"; 5 | 6 | const dataController = { 7 | kubectlInstall: (req: Request, res: Response, next: NextFunction) => { 8 | const version: SpawnSyncReturns = spawnSync( 9 | "kubectl", 10 | ["version"], 11 | { 12 | encoding: "utf-8", 13 | shell: true, 14 | } 15 | ); 16 | const client = version.output[1].split(" "); 17 | if (client[0] !== "Client") { 18 | res.locals.kubeInstalled = false; 19 | return next(); 20 | } else { 21 | return next(); 22 | } 23 | }, 24 | 25 | getName: (req: Request, res: Response, next: NextFunction) => { 26 | const data: Data = { clusterName: "" }; 27 | const name: SpawnSyncReturns = spawnSync( 28 | "kubectl", 29 | ["config", "current-context"], 30 | { 31 | encoding: "utf-8", 32 | } 33 | ); 34 | 35 | data.clusterName = name.stdout.split("\n")[0]; 36 | res.locals.data = data; 37 | return next(); 38 | }, 39 | 40 | getNodeData: (req: Request, res: Response, next: NextFunction) => { 41 | if (res.locals.data.clusterName === "") { 42 | return next(); 43 | } 44 | const nodeArr: NodeData[] = []; 45 | const data: SpawnSyncReturns = spawnSync( 46 | "kubectl", 47 | ["top", "nodes"], 48 | { 49 | encoding: "utf-8", 50 | } 51 | ); 52 | let dataArr: string[] = data.stdout.split("\n"); 53 | dataArr = dataArr.slice(1, -1); 54 | dataArr.forEach((el) => { 55 | const regex = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/; 56 | const matches = el.match(regex); 57 | const resultArray = matches ? matches.slice(1) : []; 58 | let color = ""; 59 | let cpu: number | string = resultArray[2].slice(0, -1); 60 | cpu = Number(cpu); 61 | let mem: number | string = resultArray[4].slice(0, -1); 62 | mem = Number(mem); 63 | if (mem > 70 || cpu > 70) { 64 | color = "red"; 65 | } else if (mem > 50 || cpu > 50) { 66 | color = "rgb(252, 245, 95)"; 67 | } else { 68 | color = "rgb(144, 238, 144)"; 69 | } 70 | const nodeData: NodeData = { 71 | name: resultArray[0], 72 | cpuCores: resultArray[1], 73 | memBytes: resultArray[3], 74 | cpuPercentage: resultArray[2], 75 | memPercentage: resultArray[4], 76 | color: color, 77 | }; 78 | nodeArr.push(nodeData); 79 | }); 80 | res.locals.data.nodes = nodeArr; 81 | return next(); 82 | }, 83 | 84 | getPodData: (req: Request, res: Response, next: NextFunction) => { 85 | if (res.locals.data.clusterName === "") { 86 | return next(); 87 | } 88 | const nodeData: NodeData[] = res.locals.data.nodes; 89 | nodeData.forEach((el) => { 90 | const nodeName = el.name; 91 | const podsData: SpawnSyncReturns = spawnSync( 92 | "kubectl", 93 | ["get", "pods", "--field-selector", `spec.nodeName=${nodeName}`], 94 | { 95 | encoding: "utf-8", 96 | } 97 | ); 98 | let podNames = podsData.stdout.match(/^\S+/gm); 99 | if (podNames) podNames.shift(); 100 | el.pods = podNames || []; 101 | }); 102 | res.locals.data.nodes = nodeData; 103 | return next(); 104 | }, 105 | }; 106 | 107 | export = dataController; 108 | -------------------------------------------------------------------------------- /server/controllers/dataControllerNew.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync, SpawnSyncReturns } from "child_process"; 2 | import { Request, Response, NextFunction } from "express"; 3 | import { Data, NodeData } from "../types"; 4 | const k8s = require("@kubernetes/client-node"); 5 | 6 | const dataControllerNew = { 7 | main: async (req: Request, res: Response, next: NextFunction) => { 8 | const kc = new k8s.KubeConfig(); 9 | kc.loadFromDefault(); 10 | 11 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 12 | try { 13 | const podsRes = await k8sApi.listNode("default"); 14 | const result = []; 15 | console.log(podsRes.body); 16 | podsRes.response.body.items.forEach((el) => { 17 | result.push({ 18 | name: el.metadata.name, 19 | uid: el.metadata.uid, 20 | creationTimestamp: el.metadata.creationTimestamp, 21 | capacity: { 22 | cpuCapacity: el.status.capacity.cpu, 23 | memoryCapacity: el.status.capacity.memory, 24 | podsCapacity: el.status.capacity.pods, 25 | }, 26 | allocatable: { 27 | cpuAvailable: el.status.allocatable.cpu, 28 | memoryAvailable: el.status.allocatable.memory, 29 | podsAvailable: el.status.allocatable.pods, 30 | }, 31 | conditions: el.status.conditions, 32 | totalImages: el.status.images.length, 33 | }); 34 | }); 35 | console.log(result); 36 | res.locals.chart = result 37 | 38 | } catch (err) { 39 | console.error(err); 40 | } 41 | return next(); 42 | }, 43 | 44 | 45 | }; 46 | 47 | export = dataControllerNew; 48 | -------------------------------------------------------------------------------- /server/routes/awsRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Router, Request, Response } from "express"; 2 | import * as awsController from "../controllers/awsController"; 3 | 4 | const awsRouter: Router = express.Router(); 5 | 6 | awsRouter.post( 7 | "/", 8 | awsController.isAWSCLIInstalled, 9 | awsController.awsConfigure, 10 | awsController.awsMetrics, 11 | (req: Request, res: Response) => { 12 | if (res.locals.awsInstalled === true) { 13 | console.log("hello"); 14 | res.status(200).send("success"); 15 | } else if (res.locals.awsInstalled === false) { 16 | console.log("hi"); 17 | res.status(404).send("failure"); 18 | } 19 | } 20 | ); 21 | 22 | // should come with req.body {name: demoAKS resource-group: aksRG} 23 | awsRouter.post("/auth", awsController.awsAuthenticate, (req: Request, res: Response) => { 24 | console.log("auth leaves backend"); 25 | res.status(200).send("success"); 26 | }); 27 | 28 | export = awsRouter; 29 | -------------------------------------------------------------------------------- /server/routes/azRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Router, Request, Response } from "express"; 2 | import * as azController from "../controllers/azController"; 3 | 4 | const azRouter: Router = express.Router(); 5 | 6 | azRouter.get( 7 | "/", 8 | azController.isAzureCliInstalled, 9 | // azController.installAzureCli, 10 | azController.azLogin, 11 | (req: Request, res: Response) => { 12 | console.log("this is res.locals in azcontroller", res.locals); 13 | // prompt user for resource group and cluster name 14 | console.log( 15 | "This is res.locals.isAzureCliInstalled: ", 16 | res.locals.azInstalled 17 | ); 18 | if (res.locals.azInstalled) { 19 | res.status(200).send("success"); 20 | } else { 21 | res.status(404).send("failure"); 22 | } 23 | } 24 | ); 25 | 26 | // should come with req.body {name: demoAKS resource-group: aksRG} 27 | azRouter.post("/", azController.azCredentials, (req: Request, res: Response) => { 28 | if (res.locals.formsuccess === false) { 29 | res.status(404).send("failure"); 30 | } else { 31 | res.status(200).send("success"); 32 | } 33 | }); 34 | 35 | export = azRouter; 36 | -------------------------------------------------------------------------------- /server/routes/dataRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Router, Request, Response } from "express"; 2 | import * as dataController from "../controllers/dataController"; 3 | import * as dataControllerNew from "../controllers/dataControllerNew"; 4 | 5 | const dataRouter: Router = express.Router(); 6 | 7 | // Default route to pull node data if the user has a current kubeconfig file 8 | dataRouter.get( 9 | "/", 10 | dataController.kubectlInstall, 11 | (req: Request, res: Response) => { 12 | if (res.locals.kubeInstalled === false) { 13 | res.status(200).send(false); 14 | } else { 15 | res.redirect("getData/nodes"); 16 | } 17 | } 18 | ); 19 | 20 | dataRouter.get( 21 | "/nodes", 22 | dataController.getName, 23 | dataController.getNodeData, 24 | dataController.getPodData, 25 | (req: Request, res: Response) => { 26 | res.json(res.locals.data); 27 | } 28 | ); 29 | 30 | dataRouter.get( 31 | "/api", 32 | dataControllerNew.main, 33 | (req: Request, res: Response) => { 34 | res.json(res.locals.chart); 35 | }) 36 | 37 | export = dataRouter; 38 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Application, Request, Response, NextFunction } from "express"; 2 | import cors from "cors"; 3 | import path from "path"; 4 | import { Router } from "express"; 5 | 6 | const app: Application = express(); 7 | 8 | const dataRouter: Router = require("./routes/dataRouter"); 9 | const azRouter: Router = require("./routes/azRouter"); 10 | const awsRouter: Router = require("./routes/awsRouter"); 11 | 12 | const PORT: number = 3000; 13 | 14 | // Parse JSON incoming 15 | app.use(express.json()); 16 | 17 | // Accept requests from any domain - to be updated 18 | app.use(cors({ origin: "*" })); 19 | 20 | // Serve static files and the index.html file 21 | app.use("/", express.static(path.join(__dirname, "../client"))); 22 | app.get("/", function (req: Request, res: Response) { 23 | res.sendFile(path.resolve(__dirname, "../client/index.html")); 24 | }); 25 | 26 | // Routers 27 | app.use("/getData", dataRouter); 28 | app.use("/azlogin", azRouter); 29 | app.use("/awslogin", awsRouter); 30 | 31 | // Serve 404 error to all other unknown routes 32 | app.use("*", (req: Request, res: Response) => 33 | res.status(404).send("Page not found") 34 | ); 35 | 36 | // Global error handler 37 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 38 | const defaultErr = { 39 | log: "Express error handler caught an unknown middleware error, uh-oh!", 40 | status: 500, 41 | message: { err: "An error occurred" }, 42 | }; 43 | // Use default err mashed with changes from passed-in err 44 | const errorObj = Object.assign(defaultErr, err); 45 | console.log(errorObj.log); 46 | return res.status(errorObj.status).send(errorObj.message); 47 | }); 48 | 49 | // Listen for port 50 | app.listen(PORT, () => console.log(`Server listening on port: ${PORT}`)); 51 | 52 | // Uncomment this \/ for testing 53 | // export default app; -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | export type NodeData = { 2 | name: string; 3 | cpuCores: string; 4 | memBytes: string; 5 | cpuPercentage: string; 6 | memPercentage: string; 7 | color: string; 8 | pods?: string[]; 9 | } 10 | 11 | export type Data = { 12 | clusterName: string; 13 | nodes?: NodeData[]; 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "outDir": "./dist", 6 | "rootDir": "./server", //specifices the root directory of the TS source files 7 | "esModuleInterop": true, //allows compatibility with modules that use 'export =' 8 | "declaration": true, 9 | "sourceMap": true, 10 | "moduleResolution": "node", 11 | "types": ["node", "express"], 12 | "lib": ["es2018"], 13 | }, 14 | "include":["server/**/*.ts"], 15 | "exclude": ["node_modules"] //no need to compile or look at node modules 16 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HTMLWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: "./client/index.js", 6 | output: { 7 | path: path.join(__dirname, "/dist"), 8 | filename: "bundle.js", 9 | publicPath: "/", 10 | clean: true, 11 | }, 12 | 13 | plugins: [ 14 | new HTMLWebpackPlugin({ 15 | template: path.resolve(__dirname, "./client/index.html"), 16 | }), 17 | ], 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(ts|tsx)$/, // Add TypeScript file extensions 23 | exclude: /node_modules/, 24 | use: { 25 | loader: "ts-loader", 26 | }, 27 | }, 28 | { 29 | test: /\.(js|jsx)$/, 30 | exclude: /node_modules/, 31 | use: { 32 | loader: "babel-loader", 33 | options: { 34 | presets: ["@babel/preset-env", "@babel/preset-react"], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.scss$/, 40 | use: ["style-loader", "css-loader", "sass-loader"], 41 | }, 42 | { 43 | test: /\.(png|jpe?g|gif)$/i, 44 | use: [ 45 | { 46 | loader: "file-loader", 47 | }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | devServer: { 53 | static: { 54 | directory: path.join(__dirname, "build"), 55 | }, 56 | port: 8080, 57 | historyApiFallback: true, 58 | proxy: { 59 | "/": "http://localhost:3000", 60 | }, 61 | }, 62 | }; 63 | --------------------------------------------------------------------------------