├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Tech Stack
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 |
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 |
77 |
Cameron Blair
78 |
79 |
80 |
88 |
John Norlin
89 |
90 |
98 |
99 |
100 |
Hugh Stapleton
101 |
102 |
110 |
Nick Vanderlinden
111 |
112 |
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 |
53 |
54 | Please select a Node
55 |
56 | {chartData.map((node) => (
57 |
58 | {node.name}
59 |
60 |
61 | ))}
62 |
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 |
127 | Status: {condition.status}
128 | lastHeartbeatTime: {condition.lastHeartbeatTime}
129 | lastTransitionTime: {condition.lastTransitionTime}
130 | reason: {condition.reason}
131 | message: {condition.message}
132 |
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 | Node Name:
22 |
23 |
24 | {nodeName ? nodeName[3] : null}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | CPU:
36 |
37 |
38 | {' '}
39 | {data ? data.cpuPercentage : null}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | CPU Cores:
50 |
51 |
52 | {data ? data.cpuCores : null}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Memory:
65 |
66 |
67 | {data ? data.memPercentage : null}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | Memory Bytes:
78 |
79 |
80 | {data ? data.memBytes : null}
81 |
82 |
83 |
84 |
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 |
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 |
18 |
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 |
27 |
28 | Windows:
29 |
30 |
31 | Download the MSI installer from{" "}
32 | here and follow the
33 | installation wizard.
34 |
35 |
36 |
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 |
{
92 | setInstall(false);
93 | }}
94 | >
95 | AWS CLI Installed
96 |
97 | ) : null}
98 | {auth ? (
99 |
106 | ) : null}
107 | {chooseForm ? (
108 |
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 |
13 | Linux:
14 |
15 |
16 | curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
17 |
18 |
19 |
20 |
21 | macOS:
22 |
23 |
24 | brew update && brew install azure-cli
25 |
26 |
27 |
28 |
29 | Windows:
30 |
31 |
32 | Download the MSI installer from{' '}
33 | here and follow the installation wizard.
34 |
35 |
36 |
37 |
38 |
dispatch(aksCLIInfo())}>Azure CLI Installed
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 | {
94 | dispatch(cloudInfo());
95 | dispatch(showPrompt());
96 | }}
97 | >
98 | Connect to Cloud Cluster
99 |
100 | {
104 | dispatch(localInfo());
105 | dispatch(showPrompt());
106 | }}
107 | >
108 | Connect to Local Cluster
109 |
110 |
111 | ) : null}
112 |
113 | {cloud ? (
114 |
115 |
116 | {aksCLI ?
: null}
117 |
{
122 | AKSfetch();
123 | }}
124 | >
125 | Connect to AKS-hosted Cluster
126 |
127 |
128 |
129 | {/* {awsCLI ?
: null} */}
130 |
131 |
{
136 | dispatch(aws());
137 | dispatch(cloudInfo());
138 | }}
139 | >
140 | Connect to AWS-hosted Cluster
141 |
142 |
143 |
144 | ) : null}
145 | {awsForm ?
: null}
146 | {local ? (
147 |
148 |
149 |
150 | ) : null}
151 |
152 | {aks ? (
153 |
154 |
161 | {result === true ? (
162 |
163 | Successfully added a Kube config file, please try to Render Node
164 | Map!
165 | {
169 | dispatch(aksForm());
170 | // dispatch(showPrompt());
171 | }}
172 | >
173 | Done!
174 |
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 |
getData()}
188 | >
189 | Render Node Map
190 |
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 |
dispatch(kubectlSet())}>
27 | Reconnect Cluster
28 |
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 |
16 | Install a Local Kubernetes Cluster:
17 | {/* Instructions for installing a local cluster */}
18 |
19 |
20 | Start the Local Cluster:
21 | {/* Instructions for starting the local cluster */}
22 |
23 |
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 |
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 |
dispatch(localInfo())}
45 | >
46 | Local Cluster Setup Complete
47 |
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 |
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 |
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 |
--------------------------------------------------------------------------------