├── src ├── assets │ └── kubernetes.png ├── kObjects │ ├── kObject.ts │ ├── egressPolicy.ts │ ├── volumeClaimTemplates.ts │ ├── kReplicaSet.ts │ ├── kDaemonSet.ts │ ├── container.ts │ ├── ingressPolicy.ts │ ├── statefulContainer.ts │ ├── kPod.ts │ ├── kService.ts │ ├── __index.ts │ ├── kDeployment.ts │ ├── kNetworkPolicy.ts │ └── kStatefulSet.ts ├── components │ ├── views │ │ ├── NotLiveMessage.tsx │ │ ├── NavBar.tsx │ │ ├── NetworkPolicyLegend.tsx │ │ ├── FetchLiveData.tsx │ │ ├── SidebarClusterView.tsx │ │ ├── PodInfoInNodeView.tsx │ │ ├── Legend.tsx │ │ ├── SidebarNodeView.tsx │ │ ├── UploadView.tsx │ │ ├── NetworkPolicyView.tsx │ │ ├── NodeView.tsx │ │ └── ClusterView.tsx │ └── App.tsx ├── index.tsx ├── index.html ├── component_data │ ├── findSelectorMatch.ts │ └── kDataParser.ts ├── preload.js ├── main.js └── scss │ ├── styles.scss │ └── GraphStyles.ts ├── teardown.js ├── decs.d.ts ├── jest.config.js ├── navigate_logs └── readme.txt ├── .babelrc ├── __tests__ ├── server.test.ts ├── FetchLiveData.test.tsx ├── SidebarClusterView.test.tsx └── runCommand.test.ts ├── .github └── workflows │ └── regressionTest.yml ├── tsconfig.json ├── LICENSE ├── server ├── parseDeployment.ts ├── runCommand.js ├── kDeploymentLive.ts ├── parser.ts ├── GetLiveData │ ├── parser.js │ └── getKubernetesData.js ├── parsePods.ts ├── logAggregator.ts ├── server.ts ├── kPodLive.ts └── databaseController.ts ├── webpack.config.js ├── .gitignore ├── README.md └── package.json /src/assets/kubernetes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/navigate/HEAD/src/assets/kubernetes.png -------------------------------------------------------------------------------- /src/kObjects/kObject.ts: -------------------------------------------------------------------------------- 1 | export interface kObject { 2 | label: string; 3 | getLabel: () => string; 4 | } -------------------------------------------------------------------------------- /teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async function () { 2 | console.log('tests finished!'); 3 | process.exit(0); 4 | } -------------------------------------------------------------------------------- /decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'cytoscape-dagre'; 2 | declare module 'cytoscape-cola'; 3 | declare module 'react-cytoscapejs'; 4 | declare module '*.scss'; 5 | declare module 'react-router-dom'; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'jsdom', 5 | globalTeardown: './teardown.js', 6 | }; -------------------------------------------------------------------------------- /navigate_logs/readme.txt: -------------------------------------------------------------------------------- 1 | This file is necessary as we currently have dependencies on a nonempty "navigate_logs" folder in the root directory of the project. 2 | We're working to have this not be the case soon :) -------------------------------------------------------------------------------- /src/components/views/NotLiveMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function NotLiveMessage() { 4 | return ( 5 |
6 |

Kubernetes engine not running, no nodes detected.

7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-transform-runtime", 10 | { 11 | "regenerator": true 12 | } 13 | ] 14 | ] 15 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import App from './components/App'; 4 | const styles = require('./scss/styles.scss'); 5 | import UploadView from './components/views/UploadView'; 6 | render ( 7 | , 8 | document.getElementById('root') 9 | ); -------------------------------------------------------------------------------- /src/kObjects/egressPolicy.ts: -------------------------------------------------------------------------------- 1 | export default class EgressPolicy{ 2 | ipBlock: string; 3 | protocol: string; 4 | port: number; 5 | 6 | constructor(ipBlock: string, protocol: string, port: number){ 7 | this.ipBlock = ipBlock; 8 | this.protocol = protocol; 9 | this.port = port; 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/kObjects/volumeClaimTemplates.ts: -------------------------------------------------------------------------------- 1 | export default class volumeClaimTemplates { 2 | name: string; 3 | accessMode: string[]; 4 | storage: any; 5 | constructor(name: string, accessMode: string[], storage: any){ 6 | this.name = name; 7 | this.accessMode = accessMode; 8 | this.storage = storage; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Navigate 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /src/kObjects/kReplicaSet.ts: -------------------------------------------------------------------------------- 1 | import type {kObject} from './kObject'; 2 | 3 | export class kReplicaSet implements kObject { 4 | label: string; 5 | replicas: number; 6 | 7 | constructor(label: string, replicas: number) 8 | { 9 | this.label = label; 10 | this.replicas = replicas; 11 | } 12 | getLabel(): string 13 | { 14 | return this.label; 15 | } 16 | } -------------------------------------------------------------------------------- /src/kObjects/kDaemonSet.ts: -------------------------------------------------------------------------------- 1 | import type {kObject} from './kObject'; 2 | 3 | export class kDaemonSet implements kObject { 4 | label: string; 5 | namespace: string; 6 | kind: string; 7 | 8 | constructor(label: string, namespace: string, kind: string) 9 | { 10 | this.label = label; 11 | this.namespace= namespace; 12 | this.kind = kind; 13 | } 14 | getLabel(): string 15 | { 16 | return this.label; 17 | } 18 | } -------------------------------------------------------------------------------- /src/component_data/findSelectorMatch.ts: -------------------------------------------------------------------------------- 1 | import { anyObject } from "../kObjects/__index"; 2 | export const findSelectorMatch = (obj1: anyObject, obj2: anyObject) => { 3 | let namespaceCount: number = 0; 4 | for(let key in obj2.selectors){ 5 | if(obj2.selectors[key] === obj1.selectors[key]){ 6 | if(obj1.selectors[key] !== obj2.namespace) 7 | return true; 8 | else namespaceCount++; 9 | if(namespaceCount > 1) return true; 10 | } 11 | else continue; 12 | } 13 | return false; 14 | } -------------------------------------------------------------------------------- /src/components/views/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function NavBar() { 5 | return ( 6 |
7 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/kObjects/container.ts: -------------------------------------------------------------------------------- 1 | export default class Container { 2 | name: string; 3 | image: string; 4 | env: env; 5 | containerPort: number; 6 | 7 | 8 | constructor(name: string, image: string, env: env, port: number){ 9 | this.name = name; 10 | this.image = image; 11 | this.env = env; 12 | this.containerPort = port; 13 | } 14 | } 15 | 16 | export class env { 17 | name: string; 18 | value: string; 19 | 20 | constructor(name: string, value: string){ 21 | this.name = name; 22 | this.value = value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/server.test.ts: -------------------------------------------------------------------------------- 1 | import {PORT} from '../server/server'; 2 | import request from 'supertest'; 3 | 4 | const server = `http://localhost:${PORT}`; 5 | 6 | describe("router unit tests", () => { 7 | 8 | describe("GET", () => { 9 | it("should return with a status code of 200", (done) => { 10 | request(server).get('/') 11 | .send() 12 | .expect(200) 13 | done(); 14 | }); 15 | 16 | it("returns an html file", (done) => { 17 | request(server).get('/') 18 | .expect('text/html; charset=UTF-8') 19 | done(); 20 | }) 21 | }) 22 | }); -------------------------------------------------------------------------------- /src/components/views/NetworkPolicyLegend.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function NetworkPolicyLegend() { 4 | return ( 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
Legend
External IP addresses
Pod
Allowed Traffic
Blocked Traffic
15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /.github/workflows/regressionTest.yml: -------------------------------------------------------------------------------- 1 | name: RegressionTest CI 2 | on: 3 | pull_request: 4 | types: [opened, reopened] 5 | branches: 6 | - 'develop' 7 | - 'main' 8 | 9 | jobs: 10 | run-tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [12.x, 14.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /src/kObjects/ingressPolicy.ts: -------------------------------------------------------------------------------- 1 | export default class IngressPolicy { 2 | ipBlock: string; 3 | except: string; 4 | namespaceSelectors: Array; 5 | podSelectors: Array; 6 | protocol: string; 7 | port: number; 8 | 9 | constructor(ipBlock: string, except: string, namespaceSelectors: Array, podSelectors: Array, protocol: string, port: number){ 10 | this.ipBlock = ipBlock; 11 | this.except = except; 12 | this.namespaceSelectors = namespaceSelectors; 13 | this.podSelectors = podSelectors; 14 | this.protocol = protocol; 15 | this.port = port; 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "allowSyntheticDefaultImports": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "outDir": "build", 18 | "sourceMap": true, 19 | }, 20 | "include": ["src", "decs.d.ts", "server/**/*"] 21 | } -------------------------------------------------------------------------------- /src/kObjects/statefulContainer.ts: -------------------------------------------------------------------------------- 1 | export default class statefulContainer { 2 | name: string; 3 | image: string; 4 | containerPort: number; 5 | volumeMount: volumeMount; 6 | 7 | constructor(name: string, image: string, port: number, volumeMount:volumeMount){ 8 | this.name = name; 9 | this.image = image; 10 | this.containerPort = port; 11 | this.volumeMount = volumeMount; 12 | } 13 | } 14 | 15 | export class volumeMount { 16 | mountPath: string; 17 | name: string; 18 | 19 | constructor(mountPath: string, name: string){ 20 | this.mountPath = mountPath; 21 | this.name = name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/FetchLiveData.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent, waitFor, screen} from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import FetchLiveData from '../src/components/views/FetchLiveData'; 5 | import {rest} from 'msw'; 6 | import { setupServer } from 'msw/node'; 7 | 8 | const server = setupServer( 9 | rest.get('http://localhost:3000/update', (req, res, ctx) => { 10 | return res(ctx.status(200)); 11 | }) 12 | ); 13 | 14 | beforeAll(() => server.listen()) 15 | afterEach(() => server.resetHandlers()) 16 | afterAll(() => server.close()) 17 | 18 | test('', async () => { 19 | render() 20 | expect(screen.getByRole('button')).toBeTruthy(); 21 | }); -------------------------------------------------------------------------------- /src/components/views/FetchLiveData.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | function FetchLiveData(props: any) { 4 | 5 | const reRunGetKubernetesData = () => { 6 | function requestUpdate() { 7 | fetch('http://localhost:3000/update') 8 | .then(response => console.log("Successful")) 9 | .catch(error => console.log('POST ERROR: ' + error)); 10 | } 11 | requestUpdate(); 12 | } 13 | 14 | return ( 15 |
16 | 17 |
18 | ) 19 | } 20 | 21 | export default FetchLiveData; 22 | -------------------------------------------------------------------------------- /src/kObjects/kPod.ts: -------------------------------------------------------------------------------- 1 | import type {kObject} from './kObject'; 2 | 3 | export default class kPod implements kObject { 4 | label: string; 5 | identifier: string; 6 | currentHealthState: HealthState; 7 | ownerLabel: ownerRef; 8 | 9 | constructor(label: string, identifier: string){ 10 | this.label = label; 11 | this.identifier = identifier; 12 | this.currentHealthState = HealthState.Healthy; 13 | } 14 | 15 | getLabel = (): string => { 16 | return this.label; 17 | } 18 | 19 | setOwner = (reference: ownerRef): void => { 20 | this.ownerLabel = reference; 21 | } 22 | } 23 | 24 | export class ownerRef { 25 | name: string; 26 | UID: string; 27 | } 28 | 29 | export enum HealthState { 30 | Healthy, 31 | Unhealty 32 | } -------------------------------------------------------------------------------- /src/kObjects/kService.ts: -------------------------------------------------------------------------------- 1 | import type {kObject} from './kObject' 2 | export default class kService implements kObject { 3 | namespace: string; 4 | label: string; 5 | kind: string; 6 | port: number; 7 | targetPort: number; 8 | selectors: object; 9 | type: serviceType; 10 | 11 | constructor(namespace: string,label: string, kind:string, port: number, targetPort: number, selectors: object, type = serviceType.ClusterIP){ 12 | this.namespace = namespace; 13 | this.label = label; 14 | this.kind = kind; 15 | this.port = port; 16 | this.targetPort = targetPort; 17 | this.selectors = selectors; 18 | this.type = type; 19 | } 20 | getLabel(): string 21 | { 22 | return this.label; 23 | } 24 | } 25 | 26 | export enum serviceType { 27 | ClusterIP, LoadBalancer, NodePort 28 | } -------------------------------------------------------------------------------- /src/kObjects/__index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * utility script for kObjects 3 | */ 4 | 5 | //allow for one line importing 6 | export {default as container} from './container'; 7 | export {env} from './container'; 8 | export {default as kPod} from './kPod'; 9 | export {kDaemonSet} from './kDaemonSet'; 10 | export {default as kDeployment} from './kDeployment'; 11 | export {kReplicaSet} from './kReplicaSet'; 12 | export {kStatefulSet} from './kStatefulSet'; 13 | export {default as statefulContainer, volumeMount} from './statefulContainer'; 14 | export {default as volumeClaimTemplates} from './volumeClaimTemplates'; 15 | export {default as kService} from './kService'; 16 | export {default as NetworkPolicy} from './kNetworkPolicy' 17 | 18 | //allow for object param declarations 19 | export interface anyObject { 20 | [key: string]: any 21 | } 22 | -------------------------------------------------------------------------------- /src/kObjects/kDeployment.ts: -------------------------------------------------------------------------------- 1 | import container from './container'; 2 | import type {kObject} from './kObject'; 3 | 4 | export default class kDeployment implements kObject { 5 | namespace: string; 6 | kind: string 7 | label: string; 8 | podLabel: string; 9 | replicas: number; 10 | container: container; 11 | selectors: object; 12 | 13 | 14 | constructor(namespace = "", kind: string, label: string, podLabel: string, replicas: number, container: container, selectors: object) 15 | { 16 | this.namespace = namespace; 17 | this.kind = kind; 18 | this.label = label; 19 | this.podLabel = podLabel; 20 | this.replicas = replicas; 21 | this.container = container; 22 | this.selectors = selectors; 23 | } 24 | 25 | getLabel(): string 26 | { 27 | return this.label; 28 | } 29 | } -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, child; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | // All of the Node.js APIs are available in the preload process. 6 | // It has the same sandbox as a Chrome extension. 7 | window.addEventListener('DOMContentLoaded', () => { 8 | const serverFile = path.join(__dirname, '../server/server.ts') 9 | 10 | runCommand = async (cmd) => { 11 | console.log(process.env.SHELL); 12 | await exec(cmd, function (error, stdout, stderr) { 13 | if(process.env.NODE_ENV !== 'test') 14 | { 15 | console.log(stdout); 16 | if(stderr) 17 | console.log('stderr: ' + stderr); 18 | if (error !== null) { 19 | console.log('exec error: ' + error); 20 | } 21 | } 22 | }); 23 | 24 | } 25 | 26 | runCommand(`npx ts-node ${serverFile}`) 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /src/kObjects/kNetworkPolicy.ts: -------------------------------------------------------------------------------- 1 | import EgressPolicy from './egressPolicy'; 2 | import IngressPolicy from './ingressPolicy'; 3 | import type {kObject} from './kObject'; 4 | 5 | export default class kNetworkPolicy implements kObject { 6 | namespace: string; 7 | kind: string 8 | label: string; 9 | podSelectors: object; 10 | policyTypes: Array; 11 | ingressPolicy: IngressPolicy; 12 | egressPolicy: EgressPolicy; 13 | 14 | constructor(namespace: string, kind: string, label: string, podSelectors: object, policyTypes: Array, ingressPolicy: IngressPolicy, egressPolicy: EgressPolicy) 15 | { 16 | this.namespace = namespace; 17 | this.kind = kind; 18 | this.label = label; 19 | this.podSelectors = podSelectors; 20 | this.policyTypes = policyTypes; 21 | this.ingressPolicy = ingressPolicy; 22 | this.egressPolicy= egressPolicy; 23 | } 24 | 25 | getLabel(): string 26 | { 27 | return this.label; 28 | } 29 | } -------------------------------------------------------------------------------- /src/kObjects/kStatefulSet.ts: -------------------------------------------------------------------------------- 1 | import type {kObject} from './kObject'; 2 | import statefulContainer from './statefulContainer' 3 | import volumeClaimTemplates from './volumeClaimTemplates'; 4 | export class kStatefulSet implements kObject { 5 | namespace: string; 6 | kind :string; 7 | label: string; 8 | replicas: number; 9 | serviceName: string; 10 | container: statefulContainer; 11 | volumeClaimTemplates: volumeClaimTemplates; 12 | constructor(namespace= "",kind: string, label: string, replicas: number, serviceName: string, container: statefulContainer, volumeClaimTemplates: volumeClaimTemplates) 13 | { 14 | this.namespace = namespace; 15 | this.kind = kind; 16 | this.label = label; 17 | this.replicas = replicas; 18 | this.serviceName = serviceName; 19 | this.container = container; 20 | this.volumeClaimTemplates = volumeClaimTemplates; 21 | } 22 | getLabel(): string 23 | { 24 | return this.label; 25 | } 26 | } -------------------------------------------------------------------------------- /__tests__/SidebarClusterView.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render, fireEvent, waitFor, screen} from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import SidebarClusterView from '../src/components/views/SidebarClusterView'; 5 | 6 | test('', async () => { 7 | //rerender original component with props to be called when needed 8 | const { rerender } = render(); 9 | 10 | render() 11 | //should have a button called "refresh" due to FetchLiveData component 12 | expect(screen.getAllByText('refresh')).toBeTruthy(); 13 | //it should not display any events if props are empty 14 | expect(screen.getByText('Kubernetes engine not running, no nodes detected.')); 15 | //should display events if props have something 16 | rerender; 17 | expect(screen.getByText('Kind:')); 18 | }); 19 | 20 | const mockProps = [ 21 | { 22 | deploymentStatus: [ 23 | { 24 | Kind: "NetworkPolicy", 25 | Name: "app-v1", 26 | Event: "Mocked", 27 | Time: "now" 28 | } 29 | ] 30 | } 31 | ]; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brian Kang 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 | -------------------------------------------------------------------------------- /server/parseDeployment.ts: -------------------------------------------------------------------------------- 1 | import kDeploymentLive from './kDeploymentLive'; 2 | 3 | export default function parseDeploymentInformation(jsonObjs: any): Array{ 4 | const podsInfoObjs: Array = []; 5 | for(let i = 0; i < jsonObjs.length; i++){ 6 | const ele = jsonObjs[i][0]; 7 | if(ele.kind === 'Deployment'){ 8 | const infoObject = new kDeploymentLive( 9 | ele.metadata.namespace, 10 | ele.kind, 11 | ele.metadata.name, 12 | ele.metadata.creationTimestamp, 13 | ele.metadata.resourceVersion, 14 | ele.metadata.uid, 15 | ele.spec.strategy.type, 16 | ele.spec.strategy.rollingUpdate.maxSurge, 17 | ele.spec.strategy.rollingUpdate.maxUnavailable, 18 | ele.spec.template.spec.containers[0].env, 19 | ele.spec.template.spec.dnsPolicy, 20 | ele.spec.template.spec.restartPolicy, 21 | ele.spec.template.spec.schedulerName, 22 | ) 23 | podsInfoObjs.push(infoObject) 24 | } 25 | } 26 | return podsInfoObjs; 27 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: './src/index.tsx', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.(ts|js)x?$/, 11 | loader: 'babel-loader', 12 | exclude: /node_modules/, 13 | options:{ 14 | presets: ['@babel/preset-env', '@babel/preset-react', "@babel/preset-typescript"] 15 | } 16 | }, 17 | { 18 | test: /\.s[ac]ss$/, 19 | use: ['style-loader', 'css-loader', 'sass-loader'] 20 | } 21 | ], 22 | }, 23 | //this is what enables users to leave off the extension when importing 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js', '.jsx'] 26 | }, 27 | performance: { 28 | hints: false, 29 | maxEntrypointSize: 512000, 30 | maxAssetSize: 512000 31 | }, 32 | // publicPath is causing 404 errors if incorrect 33 | output: { 34 | filename: 'bundle.js', 35 | path: path.resolve(__dirname, 'build'), 36 | publicPath: 'auto' 37 | }, 38 | devServer: { 39 | proxy: {'/api': 'http://localhost:3000'}, 40 | }, 41 | plugins: [ 42 | new HtmlWebpackPlugin({ 43 | template: path.join(__dirname, "src", "index.html"), 44 | }), 45 | ], 46 | } -------------------------------------------------------------------------------- /__tests__/runCommand.test.ts: -------------------------------------------------------------------------------- 1 | import exportObj from '../server/runCommand'; 2 | import * as kubernetesData from '../server/GetLiveData/getKubernetesData'; 3 | import fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | describe("run sh scripts from node", () => { 7 | 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | exportObj.runCommand(`${exportObj.command} -n mafia &> ./navigate_logs/${exportObj.fileName}`); 13 | xit("should create a txt file", (done) => { 14 | expect(fs.promises.access(path.join(__dirname, '../navigate_logs/')+exportObj.fileName, fs.constants.R_OK | fs.constants.W_OK)).toBeTruthy(); 15 | done(); 16 | }); 17 | 18 | xit("should get an array of pod names", (done) => { 19 | const parseSpy = jest.mock(path.join(__dirname, '../server/GetLiveData/getKubernetesData')); 20 | let readFileCallback: Function; 21 | jest.spyOn(fs, 'readFile').mockImplementation((path: fs.PathOrFileDescriptor, callback) => { 22 | readFileCallback = callback; 23 | }); 24 | kubernetesData.parsePodNames(); 25 | const someError = new Error('read file failed'); 26 | expect(() => readFileCallback(someError, null)).toThrowError(); 27 | expect(fs.readFile).toBeCalledWith(path.join(__dirname, `../navigate_logs/${exportObj.fileName}`), 'utf-8', expect.any(Function)); 28 | done(); 29 | }); 30 | }); -------------------------------------------------------------------------------- /server/runCommand.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec, child; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const exportObj = {}; 6 | 7 | //default command 8 | exportObj.command = `kubectl get pods -o=jsonpath='{.items[*].metadata.name}'`; 9 | exportObj.fileName = 'podNames.txt' 10 | 11 | exportObj.runCommand = (cmd) => { 12 | exec(cmd, function (error, stdout, stderr) { 13 | if(process.env.NODE_ENV !== 'test') 14 | { 15 | console.log(stdout); 16 | if(stderr) 17 | console.log('stderr: ' + stderr); 18 | if (error !== null) { 19 | console.log('exec error: ' + error); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | exportObj.runAndSave = (cmd, callback) => { 26 | var child_process = exec(cmd, function (err, stdout, stderr) { 27 | if (err && err.length > 1) { 28 | console.log("failed to execute"); 29 | callback(error("InternalError", "No input or output devices found", 500)); 30 | return; 31 | }else{ 32 | if(stdout){ 33 | callback(null,stdout); 34 | } 35 | if(stderr){ 36 | callback(stderr); 37 | } 38 | 39 | } 40 | }); 41 | return child_process; 42 | } 43 | 44 | module.exports = exportObj; -------------------------------------------------------------------------------- /server/kDeploymentLive.ts: -------------------------------------------------------------------------------- 1 | import * as kObjects from '../src/kObjects/__index'; 2 | 3 | export default class kDeploymentLive { 4 | namespace: string; 5 | kind: string 6 | name: string; 7 | 8 | created: string; 9 | resourceVersion: string; 10 | uid: string; 11 | strategyType: string; 12 | rollingUpdateMaxSurge: string | number; 13 | rollingUpdateMaxUnavailable: string | number; 14 | env: Array; 15 | dnsPolicy: string; 16 | restartPolicy: string; 17 | schedulerName: string; 18 | 19 | constructor(namespace = "", kind: string, name: string, created: string, resourceVersion: string,uid: string, 20 | strategyType: string, rollingUpdateMaxSurge: string | number, rollingUpdateMaxUnavailable: string | number, env: Array, dnsPolicy: string, 21 | restartPolicy: string, schedulerName: string){ 22 | this.namespace = namespace; 23 | this.kind = kind; 24 | this.name = name; 25 | 26 | this.created = created; 27 | this.resourceVersion = resourceVersion; 28 | this.uid = uid; 29 | this.strategyType = strategyType; 30 | this.rollingUpdateMaxSurge = rollingUpdateMaxSurge; 31 | this.rollingUpdateMaxUnavailable = rollingUpdateMaxUnavailable; 32 | this.env = env; 33 | this.dnsPolicy = dnsPolicy; 34 | this.restartPolicy = restartPolicy; 35 | this.schedulerName = schedulerName; 36 | } 37 | } -------------------------------------------------------------------------------- /server/parser.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as YAML from 'js-yaml'; 3 | import * as path from 'path'; 4 | 5 | let data: object[]; 6 | 7 | function getFiles(fileType: RegExp, root: string): Array { 8 | const raw: string[] = []; 9 | fs.readdirSync(root).forEach(file => { 10 | if(file.match(fileType)) 11 | raw.push(file); 12 | }); 13 | const fileObjs: object[] = []; 14 | raw.forEach(file => { 15 | fileObjs.push(YAML.loadAll(fs.readFileSync(path.join(root, file), 'utf-8'))); 16 | }) 17 | return fileObjs; 18 | } 19 | 20 | function readFile(file: string): object { 21 | return YAML.loadAll(file); 22 | } 23 | 24 | function getYAMLFiles(localPath: string = '../yaml_files'): object[] { 25 | try{ 26 | const root = path.join(__dirname, localPath); 27 | data = getFiles(/\.ya?ml/, root); 28 | } 29 | catch (error) { 30 | console.log('Error with getYAMLData funciton: ', error); 31 | } 32 | return data; 33 | } 34 | 35 | function getJSONFiles(localPath: string = '../navigate_logs'): object[] { 36 | try { 37 | const root = path.join(__dirname, localPath); 38 | data = getFiles(/\.json/, root) 39 | } catch (error) { 40 | console.log('Error with getJSONData funciton: ', error) 41 | } 42 | return data; 43 | } 44 | 45 | const parser = { 46 | getYAMLFiles, 47 | getJSONFiles, 48 | readFile, 49 | } 50 | 51 | export default parser; 52 | -------------------------------------------------------------------------------- /server/GetLiveData/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var fs = require("fs"); 4 | var YAML = require("js-yaml"); 5 | var path = require("path"); 6 | var data; 7 | function getFiles(fileType, root) { 8 | var raw = []; 9 | fs.readdirSync(root).forEach(function (file) { 10 | if (file.match(fileType)) 11 | raw.push(file); 12 | }); 13 | var fileObjs = []; 14 | raw.forEach(function (file) { 15 | fileObjs.push(YAML.loadAll(fs.readFileSync(path.join(root, file), 'utf-8'))); 16 | }); 17 | return fileObjs; 18 | } 19 | function readFile(file) { 20 | return YAML.loadAll(file); 21 | } 22 | function getYAMLFiles(localPath) { 23 | if (localPath === void 0) { localPath = '../yaml_files'; } 24 | try { 25 | var root = path.join(__dirname, localPath); 26 | data = getFiles(/\.ya?ml/, root); 27 | } 28 | catch (error) { 29 | console.log('Error with getYAMLData funciton: ', error); 30 | } 31 | return data; 32 | } 33 | function getJSONFiles(localPath) { 34 | if (localPath === void 0) { localPath = '../navigate_logs'; } 35 | try { 36 | var root = path.join(__dirname, localPath); 37 | data = getFiles(/\.json/, root); 38 | } 39 | catch (error) { 40 | console.log('Error with getJSONData funciton: ', error); 41 | } 42 | return data; 43 | } 44 | var parser = { 45 | getYAMLFiles: getYAMLFiles, 46 | getJSONFiles: getJSONFiles, 47 | readFile: readFile 48 | }; 49 | exports["default"] = parser; 50 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require('electron') 2 | const path = require('path'); 3 | 4 | const createWindow = () => { 5 | // Create the browser window. 6 | const mainWindow = new BrowserWindow({ 7 | width: 1200, 8 | height: 1000, 9 | webPreferences: { 10 | preload: path.join(__dirname, 'preload.js'), 11 | nodeIntegration: true 12 | }, 13 | }) 14 | 15 | // mainWindow.webContents.setDevToolsWebContents(devtools.webContents); 16 | // mainWindow.webContents.openDevTools(); 17 | 18 | mainWindow.loadFile('build/index.html'); 19 | 20 | } 21 | 22 | // This method will be called when Electron has finished 23 | // initialization and is ready to create browser windows. 24 | // Some APIs can only be used after this event occurs. 25 | app.whenReady().then(() => { 26 | 27 | createWindow() 28 | 29 | app.on('activate', () => { 30 | // On macOS it's common to re-create a window in the app when the 31 | // dock icon is clicked and there are no other windows open. 32 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 33 | }) 34 | }) 35 | 36 | // Quit when all windows are closed, except on macOS. There, it's common 37 | // for applications and their menu bar to stay active until the user quits 38 | // explicitly with Cmd + Q. 39 | app.on('window-all-closed', () => { 40 | if (process.platform !== 'darwin') app.quit() 41 | }) 42 | 43 | // In this file you can include the rest of your app's specific main process 44 | // code. You can also put them in separate files and require them here. -------------------------------------------------------------------------------- /src/components/views/SidebarClusterView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect, useRef, useCallback, useState } from "react"; 3 | import NotLiveMessage from "./NotLiveMessage"; 4 | import {anyObject} from '../../kObjects/__index' 5 | 6 | interface IProps{ 7 | deploymentStatus?: anyObject[], 8 | namespace?: string 9 | } 10 | 11 | import FetchLiveData from "./FetchLiveData"; 12 | function SidebarClusterView(props: IProps) { 13 | let deploymentStatus = props.deploymentStatus; 14 | 15 | const statuses: any[] = []; 16 | if(deploymentStatus) 17 | deploymentStatus.forEach((ele: any, index: number) => { 18 | statuses.push(
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
Kind:{ele.kind}
Name:{ele.name}
Event:{ele.event}
Time:{ele.time}
27 |
) 28 | }) 29 | if(statuses[0] === undefined) statuses.push() 30 | 31 | return ( 32 |
33 |
34 |

Scheduler Logs

35 | 36 |
37 | 38 |
39 | {statuses} 40 |
41 |
42 | ); 43 | } 44 | 45 | export default SidebarClusterView; -------------------------------------------------------------------------------- /server/parsePods.ts: -------------------------------------------------------------------------------- 1 | import kPodLive from './kPodLive' 2 | 3 | export default function parsePodInformation(jsonObjs: any): Array{ 4 | const podsInfoObjs: Array = []; 5 | for(let i = 0; i < jsonObjs.length; i++){ 6 | const ele = jsonObjs[i][0]; 7 | if(ele.kind === 'Pod'){ 8 | const infoObject = new kPodLive( 9 | ele.metadata.namespace, 10 | ele.kind, 11 | ele.metadata.name, 12 | ele.metadata.creationTimestamp, 13 | ele.metadata.ownerReferences, 14 | ele.metadata.resourceVersion, 15 | ele.metadata.uid, 16 | ele.metadata.labels, 17 | ele.spec.containers[0].name, 18 | ele.spec.containers[0].env, 19 | ele.spec.containers[0].image, 20 | ele.spec.containers[0].imagePullPolicy, 21 | ele.spec.dnsPolicy, 22 | ele.spec.nodeName, 23 | ele.spec.premptionPolicy, 24 | ele.spec.restartPolicy, 25 | ele.spec.schedulerName, 26 | ele.spec.serviceAccount, 27 | ele.spec.serviceAccountName, 28 | ele.spec.containers[0].volumeMounts[0].name, 29 | ele.spec.containers[0].volumeMounts[0].mountPath, 30 | ele.spec.containers[0].volumeMounts[0].readOnly, 31 | ele.status.containerStatuses[0].containerID, 32 | ele.status.containerStatuses[0].imageID, 33 | ele.status.containerStatuses[0].state.running.startedAt, 34 | ele.status.hostIP, 35 | ele.status.phase, 36 | ele.status.podIP 37 | ) 38 | podsInfoObjs.push(infoObject) 39 | } 40 | } 41 | return podsInfoObjs; 42 | } -------------------------------------------------------------------------------- /server/logAggregator.ts: -------------------------------------------------------------------------------- 1 | export default function parseSchedulerInformation(jsonObjs: any){ 2 | try { 3 | const statusConditions: any = []; 4 | for(let i = 0; i < jsonObjs.length; i++){ 5 | if(jsonObjs[i][0].status === undefined) continue; 6 | for(let j = 0; j < jsonObjs[i][0].status.conditions.length; j++){ 7 | let conditionObject: any = {}; 8 | conditionObject.name = jsonObjs[i][0].metadata.name; 9 | conditionObject.kind = jsonObjs[i][0].kind; 10 | if(jsonObjs[i][0].kind === 'Deployment'){ 11 | try { 12 | conditionObject.time = jsonObjs[i][0].status.conditions[j].lastTransitionTime; 13 | conditionObject.event = jsonObjs[i][0].status.conditions[j].message; 14 | conditionObject.reason = jsonObjs[i][0].status.conditions[j].reason; 15 | conditionObject.type = jsonObjs[i][0].status.conditions[j].type; 16 | } 17 | catch (error) { 18 | console.log('Error inside deployment conditional of parseSchedulerInformation: ', error); 19 | } 20 | } 21 | else if(jsonObjs[i][0].kind === 'Pod'){ 22 | try { 23 | conditionObject.time = jsonObjs[i][0].status.conditions[j].lastTransitionTime; 24 | conditionObject.event = `Pod deployment status: ${jsonObjs[i][0].status.conditions[j].type}` 25 | } catch (error) { 26 | console.log('Error in pod conditional of parseSchedulerInformation: ', error) 27 | } 28 | } 29 | statusConditions.push(conditionObject) 30 | } 31 | } 32 | return sortStatusConditions(statusConditions); 33 | } catch (error) { 34 | console.log('Error in parseSchedulerInformation', error) 35 | } 36 | } 37 | 38 | function sortStatusConditions(statusConditions: any){ 39 | try { 40 | statusConditions.sort(function(a: any,b: any){ 41 | return a.time.localeCompare(b.time) 42 | }) 43 | return statusConditions; 44 | } catch (error) { 45 | console.log('Error in sortStatusConditions: ', error) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | build 107 | out 108 | -------------------------------------------------------------------------------- /src/components/views/PodInfoInNodeView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | function PodInfoInNodeView(props: any) { 4 | const pod = props.displayPod[0]; 5 | 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
PropertiesValues
Name{pod.name}
Kind{pod.kind}
Namespace{pod.namespace}
Node Name{pod.nodeNode}
Phase{pod.phase}
Pod IP Address{pod.podIP}
Resource Version{pod.resourceVersion}
Restart Policy{pod.restartPolicy}
Created At{pod.created}
DNS Policy{pod.dnsPolicy}
Container{pod.container}
Container ID{pod.containerID}
Container Run Started{pod.containerRunStarted}
Image{pod.image}
Image ID{pod.imageID}
Image Pull Policy{pod.imagePullPolicy}
Scheduler Name{pod.schedulerName}
Service Account{pod.serviceAccount}
Service Account Name{pod.serviceAccountName}
UID{pod.uid}
Volume Mount Path{pod.volumeMountPath}
Volume Names{pod.volumeNames}
Volume ReadOnly{pod.volumeReadOnly}
44 |
45 | ) 46 | } 47 | 48 | export default PodInfoInNodeView; -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, {Request, Response, NextFunction} from 'express'; 2 | import databaseController from './databaseController'; 3 | import path from 'path'; 4 | 5 | const app = express(); 6 | export const PORT = 3000; 7 | 8 | app.use(express.json()); 9 | app.use(express.urlencoded({ extended: true })); 10 | 11 | app.use(express.static(path.resolve(__dirname, './'))); 12 | 13 | app.use(function (req: Request, res: Response, next: NextFunction) { 14 | res.header("Access-Control-Allow-Origin", 'http://localhost:8080'); // update to match the domain you will make the request from 15 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 16 | res.status(200); 17 | next(); 18 | }); 19 | 20 | app.options("/upload", (req: Request, res: Response) => { 21 | return res.status(200).send('ok'); 22 | }); 23 | 24 | app.post("/upload", databaseController.checkLive, databaseController.parsePOST, databaseController.uploadFiles,(req: Request, res: Response) => { 25 | return res.status(200).send(res.locals.uploadedData); 26 | }); 27 | 28 | app.get("/update", databaseController.checkLive, databaseController.updateFiles, (req: Request, res: Response) => { 29 | return res.status(200).send('ok'); 30 | }); 31 | 32 | app.get("/statusConditions", databaseController.checkLive, databaseController.getLiveData, (req: Request, res: Response) => { 33 | return res.status(200).send(res.locals.pollingData); 34 | }); 35 | 36 | app.get("/getLiveDeploymentData", databaseController.checkLive, databaseController.getLiveDeploymentData, (req: Request, res: Response) => { 37 | return res.status(200).send(res.locals.podDeployData); 38 | }); 39 | 40 | app.get("/getLivePodData", databaseController.checkLive, databaseController.getLivePodData, (req: Request, res: Response) => { 41 | return res.status(200).send(res.locals.podDeployData); 42 | }); 43 | 44 | /////////// 45 | 46 | app.use("*", (req: Request, res: Response) => { 47 | return res.status(404).send("Error, path not found"); 48 | }); 49 | 50 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 51 | const errorObj = { 52 | log: "global error handler in express app", 53 | message: { err: "global error handler in express app" }, 54 | }; 55 | const errorObject = Object.assign({}, errorObj, err); 56 | console.log(errorObject); 57 | return res.status(500).json(errorObject); 58 | }); 59 | 60 | ////////// 61 | 62 | if (process.env.NODE_ENV !== 'test') { 63 | app.listen(PORT, () => { 64 | console.log(`listening on port: ${PORT}`); 65 | }); 66 | } 67 | 68 | export default app; 69 | -------------------------------------------------------------------------------- /src/components/views/Legend.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function Legend(props: any) { 4 | return props.view === "Cluster View" ? ( 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Legend

NamespaceDeployment
ServiceStateful Set
┉► Deploys/Uses┉┉Uses
━► Deploys
16 |
17 | ) 18 | : 19 | ( 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

Legend

DeploymentPod
Replica SetDeployment
ServiceStateful Set
ContainerImage
┉┉Uses━► Deploys
32 |
33 | ) 34 | } -------------------------------------------------------------------------------- /server/kPodLive.ts: -------------------------------------------------------------------------------- 1 | import * as kObjects from '../src/kObjects/__index'; 2 | export default class kPodLive { 3 | namespace: string; 4 | kind: string 5 | 6 | //metadata 7 | name: string; 8 | created: string; 9 | ownerReferences: Array; 10 | resourceVersion: string | number; 11 | uid: string; 12 | labelForMatching: object; 13 | 14 | //container 15 | container: string; 16 | env: Array; 17 | image: string; 18 | imagePullPolicy: string; 19 | 20 | //spec 21 | dnsPolicy: string; 22 | nodeNode: string; 23 | premptionPolicy: string; 24 | restartPolicy: string; 25 | schedulerName: string; 26 | serviceAccount: string; 27 | serviceAccountName: string; 28 | 29 | //volume 30 | volumeNames: Array; 31 | volumeMountPath: Array; 32 | volumeReadOnly: Array; 33 | 34 | //containerStatus 35 | containerID: string; 36 | imageID: string; 37 | containerRunStarted: string; 38 | 39 | //IP 40 | hostIP: string; 41 | phase: string; 42 | podIP: string; 43 | 44 | 45 | constructor(namespace = "", kind: string, name: string, created: string,ownerReferences: Array, 46 | resourceVersion: string,uid: string, labelForMatching: object, container: string, env: Array, image: string, imagePullPolicy: string, 47 | dnsPolicy: string, nodeNode: string, premptionPolicy: string, restartPolicy: string, schedulerName: string, 48 | serviceAccount: string, serviceAccountName: string, volumeNames: Array, volumeMountPath: Array, 49 | volumeReadOnly: Array, containerID: string, imageID: string, containerRunStarted: string, hostIP: string, 50 | phase: string, podIP: string){ 51 | this.namespace = namespace; 52 | this.kind = kind; 53 | this.name = name; 54 | this.created = created; 55 | this.ownerReferences = ownerReferences; 56 | this.resourceVersion = resourceVersion; 57 | this.uid = uid; 58 | this.labelForMatching = labelForMatching; 59 | 60 | this.container = container; 61 | this.env = env; 62 | this.image = image; 63 | this.imagePullPolicy = imagePullPolicy; 64 | this.dnsPolicy = dnsPolicy; 65 | this.nodeNode = nodeNode; 66 | this.premptionPolicy = premptionPolicy; 67 | this.restartPolicy = restartPolicy; 68 | this.schedulerName = schedulerName; 69 | this.serviceAccount = serviceAccount; 70 | this.serviceAccountName = serviceAccountName; 71 | 72 | this.volumeNames = volumeNames; 73 | this.volumeMountPath = volumeMountPath; 74 | this.volumeReadOnly = volumeReadOnly; 75 | 76 | this.containerID = containerID; 77 | this.imageID = imageID; 78 | this.containerRunStarted = containerRunStarted; 79 | 80 | this.hostIP = hostIP; 81 | this.phase = phase; 82 | this.podIP = podIP; 83 | } 84 | } -------------------------------------------------------------------------------- /src/components/views/SidebarNodeView.tsx: -------------------------------------------------------------------------------- 1 | import { HtmlTagObject } from "html-webpack-plugin"; 2 | import * as React from "react"; 3 | import kDeploymentLive from '../../../server/kDeploymentLive'; 4 | import kPodLive from '../../../server/kPodLive' 5 | import PodInfoInNodeView from './PodInfoInNodeView'; 6 | import NotLiveMessage from "./NotLiveMessage"; 7 | 8 | function SidebarNodeView(props: any) { 9 | const deployObjs = props.podDeployments; 10 | const podObjs = props.podInfoObjects; 11 | // Filtering podObject list based on on-click 12 | const displayPod: Array = []; 13 | podObjs.forEach((ele: kPodLive) => { 14 | if(ele.name === props.clickedPod){ 15 | displayPod.push(ele) 16 | } 17 | }) 18 | 19 | // Displaying deployment information 20 | const deploymentMain: any = []; 21 | deployObjs.forEach((ele: kDeploymentLive) => { 22 | if(ele.name + " deployment" === props.masterNode){ 23 | deploymentMain.push( 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
PropertyValue
Name:{ele.name}
Kind:{ele.kind}
Namespace:{ele.namespace}
Created at:{ele.created}
Environment Variables: 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
NameValue
{ele.env[0].name}{ele.env[0].value}
Resource Version:{ele.resourceVersion}
Restart Policy:{ele.restartPolicy}
Strategy Type:{ele.strategyType}
Max Surge:{ele.rollingUpdateMaxSurge}
Max Unavailable:{ele.rollingUpdateMaxUnavailable}
Scheduler Name:{ele.schedulerName}
UID:{ele.uid}
56 |
57 | ) 58 | } 59 | }) 60 | let displayPodInfo = []; 61 | 62 | 63 | if(deploymentMain[0] === undefined) { 64 | deploymentMain.push(); 65 | displayPodInfo = []; 66 | } 67 | 68 | return props.clickedPod === undefined ? ( 69 |
70 | {deploymentMain} 71 |
72 | ) : ( 73 |
74 | {displayPodInfo} 75 |
76 | ) 77 | } 78 | 79 | export default SidebarNodeView; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | A visual overview of your Kubernetes cluster - with network policies, scheduler decisions, and live logs. 8 |

9 | 10 | ## Visualize your cluster before it starts running 11 | Whether the cluster is live or not, upload your YAML config files to **Navigate**. Here you can see the network structure of your cluster, observing 12 | the connections between your stateful sets, deployments, services, and other Kubernetes objects.

13 | 14 |

You can double check for mistakes!

15 | Click on the deployment nodes in your graph to zoom into the Node View, seeing the various containers and connections each deployment brings. 16 |
17 | 18 |

19 | 20 | ## Visualize all your network policies 21 | Want to check that your network policies are correct? Use the "Network Policies" tab to view them all, one at a time. 22 | ![image](https://user-images.githubusercontent.com/5425746/139293239-f8c0db76-b6df-491a-a005-563456ca9060.png) 23 | 24 | 25 | ## Get aggregated Live Logs 26 | 27 | While the cluster is live, see: 28 | - etcd/node scheduler decisions, aggregated across many different logs 29 | - live deployment data, including statuses and conditions 30 | - live pod data that you would normally only get with `kubectl` commands 31 | 32 |

33 | 34 | ## How to use Navigate 35 | Prerequisites: npm
36 | FOR WINDOWS USERS: you will need to run `npm run server & npm run dev` instead - Electron app coming soon.

37 | Here are the steps for getting started after cloning: 38 | 39 | 1. Run `npm install`, then run `npm run build` followed by `npm run both` in your terminal. 40 | 2. From there, the dev server should launch, and you will be prompted to upload your Kubernetes yaml config files. The app will display your cluster in a network graph without your Kubernetes cluster running. At this point, only deployments and pods can be interacted with in the graph. 41 | 3. To see live deployment and pod log data, your cluster needs to be currently running on your system. 42 | 4. If there are network policy objects within your files, the top left network policy tab will provide a dropdown menu so you can inspect each policy. 43 | 44 | Currently, we are working on packaging and distributing our app cross-platform via electron. Right now the alpha version for macOS is out, which you can find on our website. 45 | 46 | ## How to contribute 47 | We're actively developing! If you've found a bug or want to help, feel free to take a look at our Issues. 48 | 49 | ## Contributors 50 | 51 | Brian Kang | Joel Park | Hemwatie Persaud 52 | -------------------------------------------------------------------------------- /src/components/views/UploadView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useDropzone} from 'react-dropzone'; 3 | import App from '../App'; 4 | import NavBar from './NavBar'; 5 | import { BrowserRouter as Router, Route, Switch} from 'react-router-dom'; 6 | import NetworkPolicyView from './NetworkPolicyView'; 7 | 8 | export default function UploadView() { 9 | const yamlFiles: Array = []; 10 | const [responseArray, setArray] = React.useState([]); 11 | const [loaded, setLoaded] = React.useState(false); 12 | const [showLoading, setLoading] = React.useState(false); 13 | const onDrop = React.useCallback(acceptedFiles => { 14 | setLoading(true); 15 | acceptedFiles.forEach((file: File, index: Number, array: Array) => { 16 | let isLastElement = false; 17 | const reader = new FileReader(); 18 | 19 | //when last element is done, make a POST request 20 | isLastElement = (index === array.length - 1) 21 | 22 | reader.onabort = () => console.log('file reading was aborted'); 23 | reader.onerror = () => console.log('file reading has failed'); 24 | reader.onload = () => { 25 | const data = reader.result; 26 | if(data) 27 | yamlFiles.push(data); 28 | if(isLastElement) 29 | { 30 | postUpload(yamlFiles); 31 | } 32 | } 33 | reader.readAsText(file); 34 | }) 35 | }, []); 36 | 37 | const {getRootProps, getInputProps} = useDropzone({onDrop, multiple: true}); 38 | 39 | function postUpload(upload: Array) { 40 | fetch('http://localhost:3000/upload', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify(upload) 46 | }) 47 | .then(response => response.json()) 48 | .then(data => { 49 | setArray(data); 50 | //show App, pass the data down as props 51 | setLoaded(true); 52 | }) 53 | .catch(error => {console.log('POST ERROR: ' + error)}); 54 | } 55 | 56 | return ( 57 | loaded ? 58 |
59 | 60 | 61 |
62 | 63 | 64 | 67 | 68 | 69 | 72 | 73 | 74 |
75 |
76 |
77 | : 78 | !showLoading ? 79 |
80 | 81 |
82 | 83 | { 84 | //some logo here 85 |
86 |

Click here to upload your YAML config files!

87 |
88 | } 89 |
90 |
91 | : 92 |
93 |
Loading...
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | ); 104 | } 105 | 106 | const styles = { 107 | container: { 108 | width: '120%' 109 | } 110 | } -------------------------------------------------------------------------------- /server/databaseController.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response, NextFunction} from 'express'; 2 | import parser from "./parser"; 3 | import parseSchedulerInformation from "./logAggregator"; 4 | import parseDeploymentInformation from "./parseDeployment"; 5 | import parsePodInformation from "./parsePods" 6 | import {aggregateLogs, checkClusterLive, YAMLData} from './GetLiveData/getKubernetesData' 7 | 8 | interface someObject { 9 | [key: string]: any 10 | } 11 | 12 | const databaseController: someObject = {}; 13 | 14 | databaseController.getLiveData = (req: Request, res: Response, next: NextFunction) => { 15 | try { 16 | if(res.locals.live){ 17 | const data = parser.getJSONFiles(); 18 | const parsedData = parseSchedulerInformation(data); 19 | res.locals.pollingData = parsedData; 20 | return next(); 21 | } 22 | else return next(); 23 | } catch (error) { 24 | console.log('Error inside databaseController.getLiveData: ', error); 25 | return next(); 26 | } 27 | } 28 | 29 | databaseController.getLiveDeploymentData = (req: Request, res: Response, next: NextFunction) => { 30 | try { 31 | if(res.locals.live){ 32 | const data = parser.getJSONFiles(); 33 | const parsedData = parseDeploymentInformation(data); 34 | res.locals.podDeployData = parsedData; 35 | return next(); 36 | } 37 | else return next(); 38 | } catch (error) { 39 | console.log('Error inside databaseController.getLiveDeploymentData: ', error); 40 | return next(); 41 | } 42 | } 43 | 44 | databaseController.getLivePodData = (req: Request, res: Response, next: NextFunction) => { 45 | try { 46 | if(res.locals.live){ 47 | const data = parser.getJSONFiles(); 48 | const parsedData = parsePodInformation(data); 49 | res.locals.podDeployData = parsedData; 50 | return next(); 51 | } 52 | else return next(); 53 | } catch (error) { 54 | console.log('Error inside databaseController.getLivePodData: ', error); 55 | return next(); 56 | } 57 | } 58 | 59 | databaseController.uploadFiles = async (req: Request, res: Response, next: NextFunction) => { 60 | try { 61 | const output: any = []; 62 | req.body.forEach((ele: string) => { 63 | output.push(parser.readFile(ele)); 64 | }); 65 | res.locals.uploadedData = JSON.stringify(output); 66 | YAMLData.data = output; 67 | if(res.locals.live){ 68 | await aggregateLogs(); 69 | return next(); 70 | } 71 | else 72 | { 73 | return next(); 74 | } 75 | } catch (error) { 76 | console.log('Error in databaseController.uploadFiles: ', error) 77 | return next(); 78 | } 79 | } 80 | 81 | databaseController.updateFiles = (req: Request, res: Response, next: NextFunction) => { 82 | try { 83 | if(res.locals.live) 84 | { 85 | aggregateLogs(); 86 | } 87 | return next(); 88 | } catch (error) { 89 | console.log('Error in databaseController.updateFiles: ', error) 90 | } 91 | } 92 | 93 | databaseController.parsePOST = (req: Request, res: Response, next: NextFunction) => { 94 | try { 95 | const output: object[] = []; 96 | req.body.forEach((ele: string) => { 97 | output.push(parser.readFile(ele)); 98 | }) 99 | res.locals.output = output 100 | return next(); 101 | }catch(error){ 102 | console.log('Error in databaseController.parsePOST: ', error) 103 | return next(); 104 | } 105 | } 106 | 107 | databaseController.checkLive = async function(req: Request, res: Response, next: NextFunction) { 108 | //checkLive() is 'kubectl' so this will only throw an error if kubernetes is not running. 109 | //If this is the case, don't try to get live logs 110 | await checkClusterLive((err: Error, result: any) => { 111 | res.locals.live = !err; 112 | next(); 113 | }); 114 | } 115 | 116 | export default databaseController; -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ClusterView from './views/ClusterView'; 3 | import NodeView from "./views/NodeView"; 4 | import {kObject} from "../kObjects/kObject" 5 | import * as kObjects from "../kObjects/__index"; 6 | import * as dataParser from "../component_data/kDataParser"; 7 | 8 | interface IProps { 9 | jsonFiles: string[], 10 | } 11 | 12 | const App = (props: IProps) => { 13 | const [dataProp, SetDataProp] = React.useState([]); 14 | const [nodeViewPage, setNodeViewPage] = React.useState(false); 15 | const [view, setView] = React.useState('Cluster View') 16 | const [masterNode, setMasterNode] = React.useState("Kubernetes Cluster") 17 | const [namespace, setNamespace] = React.useState("Kubernetes Cluster") 18 | 19 | const deploymentStatus: any[] = []; 20 | const [deploymentStat, setDeployment] = React.useState([]); 21 | 22 | const podDeployments: any[] = []; 23 | const [podDeployInfo, getDeploys] = React.useState([]); 24 | const [podInfoObjects, getPods] = React.useState([]); 25 | React.useEffect(parseData, []); 26 | React.useEffect(fetchDeploymentLive, []); 27 | React.useEffect(fetchPodLive, []); 28 | React.useEffect(fetchLiveData, []); 29 | 30 | function fetchLiveData(): void { 31 | fetch('http://localhost:3000/statusConditions') 32 | .then((data: Response) => data.json()) 33 | .then((data: string[]) => { 34 | data.forEach((data: string) => { 35 | deploymentStatus.push(data); 36 | }) 37 | setDeployment(deploymentStatus) 38 | }) 39 | .catch((error) => console.log('GET /statusConditions response error: ', error)); 40 | } 41 | 42 | function fetchDeploymentLive(): void { 43 | fetch('http://localhost:3000/getLiveDeploymentData') 44 | .then((data: Response) => data.json()) 45 | .then((data: string[]) => { 46 | data.forEach((data: string) => { 47 | podDeployments.push(data); 48 | }) 49 | getDeploys(podDeployments) 50 | }) 51 | .catch((error) => console.log('GET /getLiveDeploymentData response error: ', error)); 52 | } 53 | 54 | function fetchPodLive(): void { 55 | fetch('http://localhost:3000/getLivePodData') 56 | .then((data: Response) => data.json()) 57 | .then((data: any) => { 58 | data.forEach((data: any) => { 59 | podInfoObjects.push(data); 60 | }) 61 | getPods(podInfoObjects) 62 | }) 63 | .catch((error) => console.log('GET /getLivePodData response error: ', error)); 64 | } 65 | 66 | function parseData(): void 67 | { 68 | const data = dataParser.parseData(props.jsonFiles); 69 | SetDataProp(data); 70 | } 71 | 72 | return( !nodeViewPage ? 73 |
74 |
75 | 87 |
88 | 89 |
90 | : 91 |
92 | 105 |
106 | ) 107 | } 108 | 109 | export default App; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navigate", 3 | "productName": "Navigate", 4 | "version": "1.0.0", 5 | "description": "Kubernetes visualizer and log aggregation tool for DevOps and SRE's", 6 | "main": "./src/main.js", 7 | "dependencies": { 8 | "@babel/core": "^7.15.8", 9 | "@babel/plugin-transform-runtime": "^7.15.8", 10 | "@babel/preset-env": "^7.15.8", 11 | "@babel/preset-react": "^7.14.5", 12 | "@babel/preset-typescript": "^7.15.0", 13 | "@babel/runtime": "^7.15.4", 14 | "@testing-library/jest-dom": "^5.14.1", 15 | "@testing-library/react": "^12.1.2", 16 | "@types/cytoscape": "^3.14.19", 17 | "@types/jest": "^27.0.2", 18 | "@types/react": "^17.0.27", 19 | "@types/styled-components": "^5.1.15", 20 | "@types/supertest": "^2.0.11", 21 | "babel-loader": "^8.2.2", 22 | "copyfiles": "^2.4.1", 23 | "css-loader": "^6.3.0", 24 | "cytoscape": "^3.19.1", 25 | "cytoscape-avsdf": "^1.0.0", 26 | "cytoscape-cola": "^2.5.0", 27 | "cytoscape-cose-bilkent": "^4.1.0", 28 | "cytoscape-dagre": "^2.3.2", 29 | "cytoscape-elk": "^2.0.2", 30 | "cytoscape-spread": "^3.0.0", 31 | "electron-squirrel-startup": "^1.0.0", 32 | "express": "^4.17.1", 33 | "fs": "0.0.1-security", 34 | "html-webpack-plugin": "^5.3.2", 35 | "jest": "^27.2.5", 36 | "js-yaml": "^4.1.0", 37 | "msw": "^0.35.0", 38 | "node-ts": "^5.1.2", 39 | "nodemon": "^2.0.13", 40 | "path": "^0.12.7", 41 | "react": "^17.0.2", 42 | "react-cytoscapejs": "^1.2.1", 43 | "react-dom": "^17.0.2", 44 | "react-dropzone": "^11.4.2", 45 | "react-router-dom": "^5.3.0", 46 | "sass": "^1.42.1", 47 | "sass-loader": "^12.1.0", 48 | "style-loader": "^3.3.0", 49 | "supertest": "^6.1.6", 50 | "ts-jest": "^27.0.5", 51 | "ts-node": "^10.2.1", 52 | "typescript": "^4.4.3", 53 | "wait-on": "^6.0.0", 54 | "webpack": "^5.58.0" 55 | }, 56 | "devDependencies": { 57 | "@electron-forge/cli": "^6.0.0-beta.61", 58 | "@electron-forge/maker-deb": "^6.0.0-beta.61", 59 | "@electron-forge/maker-rpm": "^6.0.0-beta.61", 60 | "@electron-forge/maker-squirrel": "^6.0.0-beta.61", 61 | "@electron-forge/maker-zip": "^6.0.0-beta.61", 62 | "@types/express": "^4.17.13", 63 | "@types/js-yaml": "^4.0.3", 64 | "@types/react-dom": "^17.0.9", 65 | "electron": "15.3.0", 66 | "ts-loader": "^9.2.6", 67 | "webpack-cli": "^4.9.0", 68 | "webpack-dev-server": "^4.3.1" 69 | }, 70 | "scripts": { 71 | "test": "jest --verbose --unhandled-rejections=strict --detectOpenHandles", 72 | "start": "electron-forge start", 73 | "build": "NODE_ENV=production webpack", 74 | "server": "ts-node server/server.ts", 75 | "electron": "electron .", 76 | "dev": "export SET NODE_OPTIONS=--openssl-legacy-provider; NODE_ENV=development webpack serve --open", 77 | "both": "npm run dev & npm run start", 78 | "compile": "tsc && copyfiles -f src/index.html build/", 79 | "make-mac": "npx @electron-forge/cli make --platform darwin", 80 | "package": "electron-forge package", 81 | "make": "npx @electron-forge/cli make" 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "git+https://github.com/oslabs-beta/navigate.git" 86 | }, 87 | "keywords": [], 88 | "author": "Brian Kang, Joel Park, and Hemwatie Persaud", 89 | "license": "ISC", 90 | "bugs": { 91 | "url": "https://github.com/oslabs-beta/navigate/issues" 92 | }, 93 | "homepage": "https://github.com/oslabs-beta/navigate#readme", 94 | "config": { 95 | "forge": { 96 | "packagerConfig": {}, 97 | "makers": [ 98 | { 99 | "name": "@electron-forge/maker-squirrel", 100 | "config": { 101 | "name": "navigate" 102 | } 103 | }, 104 | { 105 | "name": "@electron-forge/maker-zip", 106 | "platforms": [ 107 | "win32" 108 | ] 109 | }, 110 | { 111 | "name": "@electron-forge/maker-deb", 112 | "config": {} 113 | }, 114 | { 115 | "name": "@electron-forge/maker-rpm", 116 | "config": {} 117 | } 118 | ], 119 | "publishers": [] 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/component_data/kDataParser.ts: -------------------------------------------------------------------------------- 1 | import * as kObjects from "../kObjects/__index"; 2 | import { kObject } from "../kObjects/kObject"; 3 | import { appendFile } from "fs"; 4 | import IngressPolicy from "../kObjects/ingressPolicy"; 5 | import EgressPolicy from "../kObjects/egressPolicy"; 6 | 7 | let kObjArray: kObject[] = []; 8 | //kObjects.anyObject// passing in string[] now, but .kind doesnt exist on type string[] 9 | export function parseData(relevantData: any[]): kObject[] { 10 | kObjArray = []; 11 | for (let i = 0; i < relevantData.length; i++) { 12 | let eleArr = relevantData[i]; 13 | for (let j = 0; j < eleArr.length; j++) { 14 | let ele = eleArr[j]; 15 | if (ele.kind === "Deployment") { 16 | const newEnv = new kObjects.env( 17 | ele.spec.template.spec.containers[0].env 18 | ? ele.spec.template.spec.containers[0].env[0].name 19 | : "none", 20 | ele.spec.template.spec.containers[0].env 21 | ? ele.spec.template.spec.containers[0].env[0].value 22 | : "none", 23 | ); 24 | const newContainer = new kObjects.container( 25 | ele.spec.template.spec.containers[0].name, 26 | ele.spec.template.spec.containers[0].image, 27 | newEnv, 28 | ele.spec.template.spec.containers[0].ports 29 | ? ele.spec.template.spec.containers[0].ports[0].containerPort 30 | : "not specified" 31 | ); 32 | const newDeployment = new kObjects.kDeployment( 33 | ele.metadata.namespace ? ele.metadata.namespace : "default", 34 | ele.kind, 35 | ele.metadata.name, 36 | //placeholder offline podname 37 | ele.metadata.name, 38 | ele.spec.replicas 39 | ? ele.spec.replicas 40 | : 1, 41 | newContainer, 42 | ele.metadata.labels 43 | ? ele.metadata.labels 44 | : ele.spec.selector.matchLabels, 45 | ); 46 | kObjArray.push(newDeployment); 47 | } else if (ele.kind === "StatefulSet") { 48 | const newVolumeMount = new kObjects.volumeMount( 49 | ele.spec.template.spec.containers[0].volumeMounts[0].mountPath, 50 | ele.spec.template.spec.containers[0].volumeMounts[0].name 51 | ); 52 | const newStatefulContainer = new kObjects.statefulContainer( 53 | ele.spec.template.spec.containers[0].name, 54 | ele.spec.template.spec.containers[0].image, 55 | ele.spec.template.spec.containers[0].ports[0].containerPort, 56 | newVolumeMount 57 | ); 58 | const newVolumeClaimTemplates = new kObjects.volumeClaimTemplates( 59 | ele.spec.volumeClaimTemplates[0].metadata.name, 60 | ele.spec.volumeClaimTemplates[0].spec.accessModes, 61 | ele.spec.volumeClaimTemplates[0].spec.resources.requests.storage 62 | ); 63 | const newKStatefulSet = new kObjects.kStatefulSet( 64 | ele.metadata.namespace ? ele.metadata.namespace : "default", 65 | ele.kind, 66 | ele.metadata.name, 67 | ele.spec.replicas ? ele.spec.replicas : 1, 68 | ele.spec.serviceName, 69 | newStatefulContainer, 70 | newVolumeClaimTemplates 71 | ); 72 | kObjArray.push(newKStatefulSet); 73 | } else if (ele.kind === "Service") { 74 | const newkSerivce = new kObjects.kService( 75 | ele.metadata.namespace ? ele.metadata.namespace : "default", 76 | ele.metadata.name, 77 | ele.kind, 78 | ele.spec.ports[0].port, 79 | ele.spec.ports[0].targetPort, 80 | ele.spec.selector, 81 | ele.spec.type, 82 | ); 83 | kObjArray.push(newkSerivce); 84 | } else if(ele.kind === "DaemonSet"){ 85 | const newkDaemonSet = new kObjects.kDaemonSet( 86 | ele.metadata.name, 87 | ele.metadata.namespace ? ele.metadata.namespace : "default", 88 | ele.kind, 89 | //want more info? 90 | ); 91 | kObjArray.push(newkDaemonSet); 92 | } else if(ele.kind === "NetworkPolicy"){ 93 | const newIngressPolicy = new IngressPolicy( 94 | ele.spec.ingress[0].from[0].ipBlock.cidr, 95 | ele.spec.ingress[0].from[0].ipBlock.except[0], 96 | ele.spec.ingress[0].from[1].namespaceSelector.matchLabels, 97 | ele.spec.ingress[0].from[2].podSelector.matchLabels, 98 | ele.spec.ingress[0].ports[0].protocol, 99 | ele.spec.ingress[0].ports[0].port, 100 | ) 101 | const newEgressPolicy = new EgressPolicy( 102 | ele.spec.egress[0].to[0].ipBlock.cidr, 103 | ele.spec.egress[0].ports[0].protocol, 104 | ele.spec.egress[0].ports[0].port, 105 | ) 106 | const newNetworkPolicy = new kObjects.NetworkPolicy( 107 | ele.metadata.namespace, 108 | ele.kind, 109 | ele.metadata.name, 110 | ele.spec.podSelector.matchLabels, 111 | ele.spec.policyTypes, 112 | newIngressPolicy, 113 | newEgressPolicy, 114 | ) 115 | kObjArray.push(newNetworkPolicy) 116 | } else { 117 | console.log("rejected: "); //ele 118 | } 119 | } 120 | } 121 | return kObjArray; 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap"); 2 | 3 | $circle-color: rgb(86, 39, 197); 4 | $backgroundColor: rgb(26, 26, 26); 5 | 6 | * { 7 | color: rgb(255, 255, 255); 8 | font-family: "Calibri", sans-serif; 9 | } 10 | h3 { 11 | color: rgb(255, 255, 255); 12 | font-family: "Calibri", sans-serif; 13 | } 14 | html, 15 | body { 16 | // background-color: $backgroundColor; 17 | width: 100%; 18 | height: 100%; 19 | margin: 0px; 20 | background: rgb(18,18,18); 21 | } 22 | p { 23 | text-align: left; 24 | padding-left: 5%; 25 | padding-right: 5%; 26 | font-size: 1.5rem; 27 | } 28 | button { 29 | border: 2px solid white; 30 | background-color: rgb(26, 26, 26); 31 | border-radius: 5px; 32 | color: white; 33 | padding: 7px 14px; 34 | font-size: 1rem; 35 | cursor: pointer; 36 | } 37 | select { 38 | color: black; 39 | } 40 | option { 41 | color: black; 42 | } 43 | button:hover { 44 | background: #e7e7e7; 45 | color: black; 46 | } 47 | #buttonDiv { 48 | display: flex; 49 | width: 100%; 50 | flex-direction: row; 51 | justify-content: space-between; 52 | align-items: center; 53 | } 54 | h4 { 55 | margin-left: 1%; 56 | margin-top: 2%; 57 | color: #932af5; 58 | font-size: 2em; 59 | } 60 | button + h3 { 61 | margin-left: 5%; 62 | color: #932af5; 63 | font-size: 2em; 64 | } 65 | #sidebar { 66 | display: flex; 67 | align-items: flex-start; 68 | justify-content: flex-start; 69 | width: 20%; 70 | height: 100%; 71 | border: 5px solid lightgray; 72 | box-shadow: 1px 1px 5px rgba(114, 113, 113, 0.915); 73 | font-family: "Calibri", sans-serif; 74 | } 75 | .header { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | } 80 | .nodeSchedulerHeader { 81 | display: flex; 82 | justify-content: space-between; 83 | align-items: center; 84 | } 85 | #circle { 86 | display: flex; 87 | align-items: center; 88 | justify-content: space-around; 89 | } 90 | 91 | #pageView { 92 | display: flex; 93 | flex-direction: row; 94 | #pageCol { 95 | display: flex; 96 | flex-direction: column; 97 | justify-content: flex-start; 98 | } 99 | } 100 | .pageViewTest { 101 | display: flex; 102 | justify-content: flex-start; 103 | align-items: flex-start; 104 | } 105 | #nodeSidebar { 106 | display: flex; 107 | flex-direction: column; 108 | justify-content: center; 109 | align-items: flex-start; 110 | } 111 | ul { 112 | list-style-type: none; 113 | margin: 0; 114 | padding: 0; 115 | overflow: hidden; 116 | background: rgb(18,18,18); 117 | } 118 | 119 | li { 120 | float: left; 121 | } 122 | 123 | li .link { 124 | display: block; 125 | color: whitesmoke; 126 | text-align: center; 127 | padding: 14px 16px; 128 | text-decoration: none; 129 | border-style: solid; 130 | border-width: 1px; 131 | border-radius: 2px; 132 | } 133 | 134 | /* Change the link color to #111 (black) on hover */ 135 | li a:hover { 136 | color: rgb(18,18,18); 137 | background: whitesmoke; 138 | } 139 | 140 | .loader { 141 | position: relative; 142 | } 143 | 144 | .inputContainer { 145 | display: flex; 146 | flex-direction: column; 147 | justify-content: space-around; 148 | align-items: center; 149 | margin-top: 20%; 150 | .text { 151 | padding: 5%; 152 | } 153 | // border-style: solid; 154 | // border-color: gray; 155 | // border-width: 1px; 156 | // border-radius: 2px; 157 | } 158 | .outer, 159 | .middle, 160 | .inner { 161 | border: 3px solid transparent; 162 | border-top-color: $circle-color; 163 | border-right-color: $circle-color; 164 | border-radius: 50%; 165 | position: absolute; 166 | top: 50%; 167 | left: 50%; 168 | } 169 | #pageView { 170 | display: flex; 171 | align-items: center; 172 | justify-content: stretch; 173 | } 174 | .outer { 175 | width: 3.5em; 176 | height: 3.5em; 177 | margin-left: -1.75em; 178 | margin-top: -1.75em; 179 | animation: spin 2s linear infinite; 180 | } 181 | 182 | .middle { 183 | width: 2.1em; 184 | height: 2.1em; 185 | margin-left: -1.05em; 186 | margin-top: -1.05em; 187 | animation: spin 1.75s linear reverse infinite; 188 | } 189 | 190 | .inner { 191 | width: 0.8em; 192 | height: 0.8em; 193 | margin-left: -0.4em; 194 | margin-top: -0.4em; 195 | animation: spin 1.5s linear infinite; 196 | } 197 | .red { 198 | color: red; 199 | } 200 | .green { 201 | color: rgb(54, 221, 54); 202 | } 203 | .networkLabel { 204 | color: #932af5; 205 | font-size: 1.5em; 206 | } 207 | .networkLabelWhite { 208 | color: whitesmoke; 209 | font-size: 1.5em; 210 | } 211 | #podInfoTable{ 212 | display: flex; 213 | flex-direction: column; 214 | width: 300px; 215 | height: 450px; 216 | overflow: scroll; 217 | } 218 | .legendID { 219 | font-size: 1em; 220 | border-style: solid; 221 | border-width: 2px; 222 | border-color: whitesmoke; 223 | border-spacing: 7px; 224 | // width: 100%; 225 | display: flex; 226 | flex-direction: column; 227 | align-items: flex-start; 228 | justify-content: flex-start; 229 | table-layout: relative; 230 | } 231 | #policyLegendID { 232 | border-style: solid; 233 | border-width: 2px; 234 | border-color: whitesmoke; 235 | display: flex; 236 | flex-direction: column; 237 | align-items: flex-start; 238 | justify-content: flex-start; 239 | } 240 | th,tr { 241 | padding-left: 5px; 242 | font-size: 1em; 243 | width:50%; 244 | } 245 | td { 246 | width: 50%; 247 | } 248 | @keyframes spin { 249 | to { 250 | transform: rotate(360deg); 251 | } 252 | } 253 | #policies { 254 | color: whitesmoke; 255 | height: 25px; 256 | width: 100%; 257 | font-weight: bold; 258 | font-size: 1em; 259 | background-color: $backgroundColor; 260 | } 261 | option { 262 | color: whitesmoke; 263 | } 264 | .sidebarTest2 { 265 | display: flex; 266 | flex-direction: column; 267 | align-items: flex-start; 268 | // flex-grow: 0.5; 269 | width: 30%; 270 | height: 70%; 271 | 272 | } 273 | .pageViewTest2 { 274 | display: flex; 275 | justify-content: space-between; 276 | align-items: center; 277 | flex-shrink: 1; 278 | width: 100%; 279 | } 280 | .imgCircle { 281 | width: auto; 282 | height: auto; 283 | border-radius: 50%; 284 | background-color: whitesmoke; 285 | } 286 | .inputView { 287 | display: flex; 288 | flex-direction: column; 289 | justify-content: center; 290 | align-items: center; 291 | } 292 | .inputContainer { 293 | display: flex; 294 | flex-direction: column; 295 | justify-content: center; 296 | align-items: center; 297 | } 298 | .uploadButton { 299 | margin: auto; 300 | text-align: center; 301 | padding-top: 1px; 302 | padding-bottom: 1px; 303 | border-width: 1px; 304 | border-style: solid; 305 | border-color: whitesmoke; 306 | border-radius: 2px; 307 | } 308 | .uploadButton:hover { 309 | color: black; 310 | background-color: rgb(172, 172, 172); 311 | } 312 | #cytoscapeDiv{ 313 | width: 70%; 314 | height: 70%; 315 | display:flex; 316 | flex-grow:1; 317 | } 318 | tbody{ 319 | width:50%; 320 | font-size: 1em; 321 | } -------------------------------------------------------------------------------- /src/components/views/NetworkPolicyView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Cytoscape from 'cytoscape'; 3 | import SidebarClusterView from "./SidebarClusterView"; 4 | import dagre from 'cytoscape-dagre'; 5 | import cola from 'cytoscape-cola'; 6 | import {GraphStyles} from "../../scss/GraphStyles"; 7 | import { electron } from "webpack"; 8 | import { anyObject } from "../../kObjects/__index"; 9 | import { findSelectorMatch } from "../../component_data/findSelectorMatch"; 10 | import kNetworkPolicy from "../../kObjects/kNetworkPolicy"; 11 | import * as dataParser from "../../component_data/kDataParser" 12 | import NetworkPolicyLegend from "./NetworkPolicyLegend"; 13 | Cytoscape.use(dagre); 14 | Cytoscape.use(cola); 15 | dagre(Cytoscape) 16 | 17 | interface IProps { 18 | jsonFiles: Array, 19 | } 20 | function NetworkPolicyView(props: IProps) { 21 | const networkPolicyRef = React.useRef(null); 22 | const relevantData: any[] = []; 23 | const [networkPolicy, setNetworkPolicy] = React.useState(''); 24 | const [networkPoliciesArr, setNetworkPoliciesArr] = React.useState([]) 25 | const networkPolicies: any[] = []; 26 | const populateNetworkPolicies = (array: any[]) => { 27 | array.forEach(kObject => { 28 | if(kObject.kind === "NetworkPolicy") networkPolicies.push(kObject); 29 | }) 30 | if(networkPolicies.length === 1) setNetworkPolicy(networkPolicies[0].label); 31 | setNetworkPoliciesArr(networkPolicies); 32 | }; 33 | function parseData(json: any[]) : anyObject[] 34 | { 35 | const data = dataParser.parseData(json); 36 | return data; 37 | } 38 | const populateArray = (array: anyObject[]): void => { 39 | for (let i = 0; i < array.length; i++) { 40 | if (array[i].kind === "NetworkPolicy" && array[i].label === networkPolicy) { 41 | const getSelectors = (obj: anyObject) => { 42 | let str = ''; 43 | for(let key in obj){ 44 | str += `${key}=${obj[key]}`; 45 | } 46 | return str; 47 | } 48 | let ingress = { 49 | data: { 50 | id: "ingress", 51 | label: `Pods labeled: ${getSelectors(array[i].podSelectors)} \n Port:${array[i].ingressPolicy.port} ${array[i].ingressPolicy.protocol}`, 52 | class: "ingress", 53 | }, 54 | }; 55 | let egress = { 56 | data: { 57 | id: "egress", 58 | label: `IPs: ${array[i].egressPolicy.ipBlock} \n Port:${array[i].egressPolicy.port} ${array[i].egressPolicy.protocol}`, 59 | class: "egress", 60 | } 61 | } 62 | let edge = { 63 | data: { 64 | source: "ingress", 65 | target: "egress", 66 | label: "allowed", 67 | }, 68 | }; 69 | let namespaceSelector = { 70 | data: { 71 | id: "namespaceSelector", 72 | label: `Pods labeled: ${getSelectors(array[i].ingressPolicy.podSelectors)} in Namespace: ${getSelectors(array[i].ingressPolicy.namespaceSelectors)}`, 73 | class: "namespacePolicy" 74 | } 75 | } 76 | let edge2 = { 77 | data: { 78 | source: "namespaceSelector", 79 | target: "ingress", 80 | label: "allowed", 81 | }, 82 | }; 83 | let ipBlock = { 84 | data: { 85 | id: "ipBlock", 86 | label: `IPs: ${array[i].ingressPolicy.ipBlock}`, 87 | class: "allowed" 88 | } 89 | } 90 | let edge4 = { 91 | data: { 92 | source: "ipBlock", 93 | target: "ingress", 94 | label: "allowed", 95 | }, 96 | }; 97 | let except = { 98 | data: { 99 | id: "except", 100 | label: `IPs: ${array[i].ingressPolicy.except}`, 101 | class: "except" 102 | } 103 | } 104 | let edge5 = { 105 | data: { 106 | source: "except", 107 | target: "ingress", 108 | label: "except", 109 | }, 110 | }; 111 | relevantData.push(ingress,egress,edge,namespaceSelector,edge2,ipBlock,edge4,except,edge5); 112 | } 113 | } 114 | } 115 | React.useEffect(() => { 116 | populateNetworkPolicies(parseData(props.jsonFiles)); 117 | populateArray(parseData(props.jsonFiles)); 118 | const config: Cytoscape.CytoscapeOptions = { 119 | container: networkPolicyRef.current, 120 | style: GraphStyles, 121 | elements: relevantData, 122 | }; 123 | let cy = Cytoscape(config); 124 | let layout = cy.layout({ 125 | fit:true, 126 | name:'dagre', 127 | nodeDimensionsIncludeLabels: true, 128 | padding: 15, 129 | animate: true, 130 | animationDuration: 750, 131 | spacingFactor: .6, 132 | }); 133 | layout.run(); 134 | cy.zoomingEnabled(false); 135 | cy.on('click',(event)=> { 136 | console.log(event.target._private.data.id); 137 | }) 138 | }, [props.jsonFiles, networkPolicy]); 139 | 140 | return( 141 |
142 |

Network Policy View

143 |
144 |
145 |
146 |
Choose a policy:
147 | 155 |
156 |

{networkPolicy}

157 | 158 |
159 |
162 |
163 |
164 |
165 | ) 166 | } 167 | // return( 168 | //
169 | //
170 | //

171 | // 172 | // {"Network Policy View"} 173 | //

174 | //
175 | //
176 | //

Choose a Network Policy

177 | // 185 | //
186 | //
187 | //

{networkPolicy}

188 | //
189 | // 190 | //
194 | //
195 | //
196 | //
197 | // ) 198 | 199 | export default NetworkPolicyView; -------------------------------------------------------------------------------- /server/GetLiveData/getKubernetesData.js: -------------------------------------------------------------------------------- 1 | const parser = require('./parser'); 2 | const exportObj = require('../runCommand'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const logPath = path.join(__dirname, `../../navigate_logs/`); 7 | 8 | const YAMLData = {data: []}; 9 | 10 | //object of the default namespaces that come with every k8s cluster; we want to ignore these 11 | const listOfDefaultNamespaces = { 12 | "kube-node-lease": '', 13 | "kube-public": '', 14 | "kube-system": '' 15 | } 16 | 17 | function getElementsOfKind(kind, writeToDisk = false) { 18 | try 19 | { 20 | const data = YAMLData.data; 21 | const output = []; 22 | data.forEach(k8sObject => { 23 | if(k8sObject[0].kind === kind){ 24 | output.push(k8sObject);} 25 | }); 26 | if(writeToDisk) 27 | fs.writeFile(path.join(logPath, `/${kind}.json`), JSON.stringify(output, null, 2), { flag: 'w' }, function(err) { 28 | if (err) 29 | return console.error(err); 30 | }); 31 | return output; 32 | } 33 | catch(error) { 34 | return error; 35 | } 36 | } 37 | 38 | //get non-default namespaces from YAML files by pushing their metadata.namespace values into an array and returning it 39 | function getNamespacesFromYAML(array){ 40 | const output = {}; 41 | array.forEach(k8sObject => { 42 | if(k8sObject[0].metadata !== undefined && k8sObject[0].metadata.namespace !== undefined) 43 | if(!output[k8sObject[0].metadata.namespace]){ 44 | output[k8sObject[0].metadata.namespace] = ''; 45 | } 46 | }); 47 | return Object.keys(output); 48 | } 49 | 50 | function getNamespaceElementPairs(kind){ 51 | const output = {}; 52 | let elements = getElementsOfKind(kind); 53 | elements.forEach(element => { 54 | if(!element[0].metadata.namespace){ 55 | if(!output["default"]) output["default"] = [element[0].metadata.name]; 56 | else output["default"].push(element[0].metadata.name); 57 | } 58 | //if it is a default namespace, skip it 59 | else if(!Object.keys(listOfDefaultNamespaces).includes(element[0].metadata.namespace)){ 60 | if(output[element[0].metadata.namespace]) output[element[0].metadata.namespace].push(element[0].metadata.name); 61 | else output[element[0].metadata.namespace] = [element[0].metadata.name]; 62 | } 63 | }) 64 | return output; 65 | } 66 | 67 | // kubectl get pods -o=jsonpath='{.items[*].metadata.name} 68 | async function parsePodNames (filePath = path.join(__dirname, `../../navigate_logs/${exportObj.fileName}`)) { 69 | const result = await fs.promises.readFile(filePath, 'utf-8', (error, result) => { 70 | if(error){ 71 | console.log(error); 72 | } 73 | }) 74 | return result; 75 | } 76 | 77 | /* 78 | 1. Get namespaces via "kubectl get namespaces" command 79 | a. Store namespaces into an array 80 | [can also get namespaces from yaml files] 81 | 2. Get deployment names via "kubectl get deployments --namespace=" command 82 | a. Iterate through all namespaces array running above command. 83 | b. Store list of deployment names as an array, inside an object, where key is 84 | [can also get deployment names from yaml files] 85 | 86 | 3. Get pod names via "kubectl get pods --namespace=" command 87 | a. Iterate through all namespaces array running above command. 88 | b. Store list of pod names as an array, inside an object, where key is 89 | 4. Get deployment logs via "kubectl get deployment -o json >> deployment[i].json" 90 | 5. Get pod logs via "kubectl get pod -o json >> pod[i].json" 91 | */ 92 | async function aggregateLogs() 93 | { 94 | await deleteFiles(path.join(__dirname, '../../navigate_logs')); 95 | //get namespace: [pods] key value pairs 96 | const namespaces = getNamespacesFromYAML(YAMLData.data); 97 | const namespacePodKVP = {}; 98 | //overwrite existing podNames txt if it exists already for first namespace, which is always "default" 99 | let rawNames; 100 | exportObj.runAndSave(`kubectl get pods -o=jsonpath=\'{.items[*].metadata.name}\' -n "default"`, 101 | (err, data) => { 102 | if(err) 103 | { 104 | console.log("failed to get pod information: ") 105 | console.log(err); 106 | } 107 | rawNames = Buffer.from(data).toString('utf8'); 108 | }).on('close', () => { 109 | const podArray = rawNames.split(' '); 110 | namespacePodKVP["default"] = podArray; 111 | console.log('all namespace names: ', namespaces); 112 | //sometimes all the pods are just in the "default" namespace, in which case this array will be empty 113 | if(namespaces[0]) 114 | for(let i = 0; i < namespaces.length; i++){ 115 | exportObj.runAndSave(`kubectl get pods -o=jsonpath=\'{.items[*].metadata.name}\' -n ${namespaces[i]}`, 116 | async (err, data) => { 117 | if(err) console.log(err); 118 | namespacePodKVP[namespaces[i]] = await Buffer.from(data).toString('utf8').split(' '); 119 | if(i === namespaces.length-1){ 120 | console.log('namespaces and pods:', namespacePodKVP); 121 | getDeployments(); 122 | getPodInfo(namespacePodKVP); 123 | } 124 | }); 125 | } 126 | }); 127 | } 128 | 129 | async function getDeployments(){ 130 | const deploymentKVP = getNamespaceElementPairs("Deployment"); 131 | const keys = Object.keys(deploymentKVP); 132 | let index = 1; 133 | // get deployment logs, saving them to disk using filePath + index 134 | for(let i = 0; i < keys.length; i++){ 135 | for(let j = 0; j < deploymentKVP[keys[i]].length; j++){ 136 | let filePath = path.join(__dirname, `../../navigate_logs/deployment${index}.json`); 137 | try 138 | { 139 | await exportObj.runCommand(`kubectl get deployment ${deploymentKVP[keys[i]][j]} --namespace=${keys[i]} -o json &> ${filePath}`); 140 | } 141 | catch(error){ 142 | console.log(error); 143 | } 144 | index++; 145 | } 146 | } 147 | } 148 | 149 | // gets updated pods object (with status from kubernetes) 150 | async function getPodInfo(namespacePodKVP){ 151 | let filePath; 152 | let index = 1; 153 | let currentKey; 154 | //loop through each key (kubernetes namespace) and each value (kubernetes pods attached to namespace) 155 | //get log data from each pod and save to disk, overwriting if previous already exist 156 | for(let i = 0; i < Object.keys(namespacePodKVP).length; i++){ 157 | currentKey = Object.keys(namespacePodKVP)[i]; 158 | for(let j = 0; j < namespacePodKVP[currentKey].length; j++) 159 | { 160 | filePath = path.join(__dirname, `../../navigate_logs/pod${index}.json`); 161 | try{ 162 | await exportObj.runCommand(`kubectl get pod ${namespacePodKVP[currentKey][j]} --namespace=${currentKey} -o json &> ${filePath}`); 163 | } 164 | catch(error){ 165 | console.log(error); 166 | } 167 | index++; 168 | } 169 | } 170 | } 171 | 172 | function deleteFiles(directory){ 173 | fs.readdir(directory, (err, files) => { 174 | if (err) throw err; 175 | let file; 176 | for (let i = 0; i < files.length; i++) { 177 | file = files[i]; 178 | //hardcode ignore readme 179 | if(file.includes('readme')) continue; 180 | fs.unlink(path.join(directory, file), err => { 181 | if (err) throw err; 182 | }); 183 | } 184 | }); 185 | } 186 | 187 | function checkClusterLive(callback){ 188 | exportObj.runAndSave('kubectl', callback); 189 | } 190 | 191 | module.exports = { 192 | parsePodNames, 193 | getElementsOfKind, 194 | getNamespaceDeploymentPairs: getNamespaceElementPairs, 195 | aggregateLogs, 196 | checkClusterLive, 197 | deleteFiles, 198 | YAMLData 199 | } -------------------------------------------------------------------------------- /src/components/views/NodeView.tsx: -------------------------------------------------------------------------------- 1 | //populate with relevant data 2 | import * as React from "react"; 3 | import Cytoscape from 'cytoscape'; 4 | import cola from 'cytoscape-cola'; 5 | import SidebarNodeView from './SidebarNodeView' 6 | import {GraphStyles} from "../../scss/GraphStyles"; 7 | import dagre from 'cytoscape-dagre' 8 | import Legend from './Legend'; 9 | import { anyObject, container } from "../../kObjects/__index"; 10 | import { kObject } from "../../kObjects/kObject"; 11 | import { findSelectorMatch } from "../../component_data/findSelectorMatch"; 12 | 13 | Cytoscape.use(cola); 14 | Cytoscape.use(dagre); 15 | 16 | 17 | function NodeView(props: any) { 18 | const nodeViewRef = React.useRef(null); 19 | let [clickedPod, registerPod] = React.useState(undefined); 20 | const relevantData: any[] = [ 21 | {data: { id: "master", label: props.masterNode , class:"namespace"}}, 22 | ]; 23 | let deploymentNode: anyObject = {}; 24 | const getMasterNode = (label: string) => { 25 | props.dataArray.forEach((obj: anyObject) => { 26 | if(`${obj.label} deployment`=== label) { 27 | return deploymentNode = obj; 28 | } 29 | }) 30 | } 31 | const populateArray = (array: any[]): void => { 32 | let targetNode; 33 | let serviceNodes: anyObject[] = []; 34 | let serviceNode: anyObject = {}; 35 | for(let i = 0; i < array.length; i++){ 36 | if(array[i].kind === "Deployment" && findSelectorMatch(array[i],deploymentNode)){ 37 | targetNode = array[i]; 38 | } 39 | if(array[i].kind === 'Service' && findSelectorMatch(array[i],deploymentNode)){ 40 | serviceNode = array[i]; 41 | serviceNodes.push(serviceNode); 42 | let newPod = { 43 | data: { 44 | id: array[i].label + ' service', 45 | label: serviceNode.port ? array[i].label + "\n" + `Port:${serviceNode.port}` : array[i].label + "\n" + `Port:N/A`, 46 | class: "service", 47 | }, 48 | }; 49 | let edge = { 50 | data: { 51 | source: 'master', 52 | target: array[i].label + ' service', 53 | label: `connection` 54 | } 55 | } 56 | relevantData.push(newPod, edge) 57 | } 58 | if(array[i].kind === 'StatefulSet'){ 59 | props.dataArray.forEach((ele: any) => { 60 | if(ele.kind === "Deployment" && (ele.label + " deployment" === props.masterNode) || (ele.selectorName + " deployment" === props.masterNode)){ 61 | if(array[i].namespace === ele.namespace){ 62 | let newPod = { 63 | data: { 64 | id: array[i].label + " stateful", 65 | label: array[i].label + "\n" + "Port:" + array[i].container.containerPort, 66 | class: "stateful", 67 | }, 68 | }; 69 | let edge = { 70 | data: { 71 | source: 'master', 72 | target: array[i].label + " stateful", 73 | label: "connection" 74 | } 75 | } 76 | relevantData.push(newPod,edge) 77 | } 78 | } 79 | }) 80 | } 81 | } 82 | const podNames: string[] = []; 83 | const containerIDs : string[] = []; 84 | for(let i = 0; i < targetNode.replicas; i++){ 85 | //get live podName 86 | props.podInfoObjects.forEach((pod: any) => { 87 | if(Object.values(pod.labelForMatching).includes(props.masterNode.split(' ')[0])){ 88 | if(!podNames.includes(pod.name)) podNames.push(pod.name); 89 | if(!containerIDs.includes(pod.containerID)) containerIDs.push(pod.containerID) 90 | } 91 | }) 92 | //create new pod 93 | let newPod = { 94 | data: { 95 | id: podNames[i] !== undefined ? podNames[i] : targetNode.podLabel + 'pod' + i, 96 | label: podNames[i] !== undefined ? podNames[i] : targetNode.podLabel + 'pod' + i, 97 | class: "pod" 98 | }, 99 | }; 100 | // line from replicaset to newPod 101 | let edge1 = { 102 | data: { 103 | source: `ReplicaSet: ${targetNode.replicas}`, 104 | target: podNames[i] !== undefined ? podNames[i] : targetNode.podLabel + 'pod' + i, 105 | label: "deployment" 106 | } 107 | } 108 | // line from service to pod 109 | if(serviceNodes){ 110 | for(let j = 0; j < serviceNodes.length; j++){ 111 | let edge2 = { 112 | data: { 113 | source: serviceNodes[j].label + " service", 114 | target: containerIDs[i] !== undefined ? containerIDs[i]: targetNode.container.name + " container" + i, 115 | label: "connection" 116 | } 117 | } 118 | relevantData.push(edge2) 119 | } 120 | } 121 | //create new container 122 | let newContainer = { 123 | data: { 124 | id: containerIDs[i] !== undefined ? containerIDs[i]: targetNode.container.name + " container" + i, 125 | label: targetNode.container.name + "\n" + "Port:" + targetNode.container.containerPort , 126 | class: "container", 127 | }, 128 | }; 129 | //line from newPod to newContainer 130 | let edge3 = { 131 | data: { 132 | source: podNames[i] !== undefined ? podNames[i] : targetNode.podLabel + 'pod' + i, 133 | target: containerIDs[i] !== undefined ? containerIDs[i]: targetNode.container.name + " container" + i, 134 | label: "deployment", 135 | } 136 | } 137 | //create new image 138 | let newImage = { 139 | data: { 140 | id: targetNode.container.image + " image" + i, 141 | label: targetNode.container.image.split(":")[0], 142 | class: "image" 143 | }, 144 | }; 145 | //line from newContainer to newImage 146 | let edge4 = { 147 | data: { 148 | source: containerIDs[i] !== undefined ? containerIDs[i]: targetNode.container.name + " container" + i, 149 | target: targetNode.container.image + " image" + i, 150 | label: "deployment" 151 | } 152 | } 153 | relevantData.push(newPod,newContainer,newImage,edge1,edge3,edge4); 154 | } 155 | const newReplicaSet = { 156 | data: { 157 | id: `ReplicaSet: ${targetNode.replicas}`, 158 | label: `ReplicaSet: ${targetNode.replicas}`, 159 | class: "replicaSet", 160 | } 161 | } 162 | const edgeReplicaSet = { 163 | data: { 164 | source: "master", 165 | target: `ReplicaSet: ${targetNode.replicas}`, 166 | label: "deployment" 167 | } 168 | } 169 | relevantData.push(newReplicaSet,edgeReplicaSet) 170 | } 171 | React.useEffect(() => { 172 | getMasterNode(props.masterNode); 173 | populateArray(props.dataArray); 174 | //Cytoscape layout and configurations 175 | const config: Cytoscape.CytoscapeOptions = { 176 | container: nodeViewRef.current, 177 | style: GraphStyles, 178 | elements: relevantData, 179 | }; 180 | let cy = Cytoscape(config); 181 | let layout = cy.layout({ 182 | name:'dagre', 183 | nodeDimensionsIncludeLabels: true, 184 | animate: true, 185 | }); 186 | layout.run(); 187 | cy.zoomingEnabled(false); 188 | cy.on('click',(event)=> { 189 | if(event.target._private.data.class === "container"){ 190 | console.log('nodeViewClick',event.target._private.data.id); 191 | } 192 | else if(event.target._private.data.class === 'pod'){ 193 | clickedPod = event.target._private.data.id; 194 | registerPod(clickedPod); 195 | } 196 | }) 197 | cy.on("mouseover","node[class = 'pod']", function(event) { 198 | event.target.style("background-image", ["https://i.ibb.co/N1fXVdp/podhover.png"]); 199 | event.target.style("background-color",'rgb(230,74,0)'); 200 | event.target.style("border-color",'rgb(230,74,0)'); 201 | event.target.style("border-width","2"); 202 | }) 203 | cy.on("mouseout","node[class = 'pod']", function(event) { 204 | event.target.style("background-image", ["https://i.ibb.co/zNx6TML/podicon.png"]); 205 | event.target.style("background-color",'white'); 206 | event.target.style("border-width","0"); 207 | }) 208 | }, []); 209 | const limitSidebarHeight = document.getElementById("clusterView")?.style.height; 210 | return ( 211 |
212 |

213 | {props.view} 214 |

215 |
216 |
217 |
218 | 225 |

{`${props.masterNode}`}

226 |
227 |
228 |
229 | 230 |
231 |
232 | 233 |
234 |
235 |
239 |
240 |
241 | 242 |
243 | 244 | ) 245 | } 246 | 247 | export default NodeView; -------------------------------------------------------------------------------- /src/components/views/ClusterView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Cytoscape from "cytoscape"; 3 | import SidebarClusterView from "./SidebarClusterView"; 4 | import dagre from "cytoscape-dagre"; 5 | import cola from "cytoscape-cola"; 6 | import { GraphStyles } from "../../scss/GraphStyles"; 7 | import Legend from "./Legend"; 8 | import { electron } from "webpack"; 9 | import { anyObject } from "../../kObjects/__index"; 10 | import { findSelectorMatch } from "../../component_data/findSelectorMatch"; 11 | import FetchLiveData from "./FetchLiveData"; 12 | Cytoscape.use(dagre); 13 | Cytoscape.use(cola); 14 | dagre(Cytoscape); 15 | 16 | function ClusterView(props: any) { 17 | const containerRef = React.useRef(null); 18 | const relevantData: any[] = [ 19 | { data: { id: "Kubernetes Cluster", label: "Control \n Plane" } }, 20 | ]; 21 | const namespacesArr: string[] = []; 22 | const populateNamespaces = (array: any[]): void => { 23 | array.forEach((kObject) => { 24 | if ( 25 | !namespacesArr.includes(kObject.namespace) && 26 | kObject.namespace !== undefined 27 | ) 28 | namespacesArr.push(kObject.namespace); 29 | }); 30 | namespacesArr.forEach((namespace) => { 31 | relevantData.push( 32 | { 33 | data: { id: namespace, label: namespace, class: "namespace" }, 34 | }, 35 | { 36 | data: { 37 | source: "Kubernetes Cluster", 38 | target: namespace, 39 | }, 40 | } 41 | ); 42 | }); 43 | }; 44 | const allLabels: any[] = []; 45 | props.dataArray.forEach((ele: any) => { 46 | if (ele.label !== undefined) allLabels.push(ele.label); 47 | }); 48 | const populateArray = (array: any[]): void => { 49 | for (let i = 0; i < array.length; i++) { 50 | if (array[i].kind === "Deployment") { 51 | let newNode = { 52 | data: { 53 | id: `${array[i].label} deployment`, 54 | label: array[i].label, 55 | class: "deployment", 56 | }, 57 | }; 58 | let edge = { 59 | data: { 60 | source: array[i].namespace, 61 | target: `${array[i].label} deployment`, 62 | label: `service`, 63 | }, 64 | }; 65 | relevantData.push(newNode, edge); 66 | } else if (array[i].kind === "StatefulSet") { 67 | let newNode = { 68 | data: { 69 | id: array[i].label, 70 | label: array[i].label, 71 | class: "stateful", 72 | }, 73 | }; 74 | let edge = { 75 | data: { 76 | source: array[i].namespace, 77 | target: array[i].label, 78 | label: `stateful`, 79 | }, 80 | }; 81 | relevantData.push(newNode, edge); 82 | props.dataArray.forEach((ele: any) => { 83 | if (ele.kind === "Service" && ele.namespace === array[i].namespace) { 84 | let edge = { 85 | data: { 86 | source: ele.label, 87 | target: array[i].label, 88 | label: `connection`, 89 | }, 90 | }; 91 | relevantData.push(edge); 92 | } 93 | }); 94 | } else if (array[i].kind === "Service") { 95 | if (!namespacesArr.includes(array[i].label)) { 96 | let newNode = { 97 | data: { 98 | id: array[i].label, 99 | label: array[i].type 100 | ? `${array[i].label} ${array[i].type}` 101 | : `${array[i].label} ClusterIP`, 102 | class: "service", 103 | }, 104 | }; 105 | props.dataArray.forEach((ele: any) => { 106 | if ( 107 | ele.kind === "Deployment" && 108 | ele.namespace === array[i].namespace && 109 | findSelectorMatch(ele, array[i]) 110 | ) { 111 | let edge = { 112 | data: { 113 | source: ele.label + " deployment", 114 | target: array[i].label, 115 | label: `connection`, 116 | }, 117 | }; 118 | let edge2 = { 119 | data: { 120 | source: array[i].namespace, 121 | target: array[i].label, 122 | label: "service", 123 | }, 124 | }; 125 | relevantData.push(edge, edge2); 126 | } 127 | }); 128 | relevantData.push(newNode); 129 | } else { 130 | let newNode = { 131 | data: { 132 | id: `${array[i].label} service`, 133 | label: array[i].type 134 | ? `${array[i].label} ${array[i].type}` 135 | : `${array[i].label} ClusterIP`, 136 | class: "service", 137 | }, 138 | }; 139 | props.dataArray.forEach((ele: any) => { 140 | if ( 141 | ele.kind === "Deployment" && 142 | ele.namespace === array[i].namespace && 143 | findSelectorMatch(ele, array[i]) 144 | ) { 145 | let edge = { 146 | data: { 147 | source: ele.label + " deployment", 148 | target: array[i].label + " service", 149 | label: `connection`, 150 | }, 151 | }; 152 | let edge2 = { 153 | data: { 154 | source: array[i].namespace, 155 | target: array[i].label + " service", 156 | label: "service", 157 | }, 158 | }; 159 | relevantData.push(edge, edge2); 160 | } 161 | }); 162 | relevantData.push(newNode); 163 | } 164 | } else if (array[i].kind === "DaemonSet") { 165 | let newDaemonSet = { 166 | data: { 167 | id: array[i].label, 168 | label: array[i].label, 169 | class: "daemonSet", 170 | }, 171 | }; 172 | let edge = { 173 | data: { 174 | source: array[i].namespace, 175 | target: array[i].label, 176 | label: `daemonSet`, 177 | }, 178 | }; 179 | relevantData.push(newDaemonSet, edge); 180 | } 181 | } 182 | }; 183 | const getNamespace = (id: string) => { 184 | for (let i = 0; i < props.dataArray.length; i++) { 185 | if (props.dataArray[i].label === id) { 186 | return props.dataArray[i].namespace; 187 | } 188 | } 189 | }; 190 | React.useEffect(() => { 191 | populateNamespaces(props.dataArray); 192 | populateArray(props.dataArray); 193 | const config: Cytoscape.CytoscapeOptions = { 194 | container: containerRef.current, 195 | style: GraphStyles, 196 | elements: relevantData, 197 | }; 198 | let cy = Cytoscape(config); 199 | let layout = cy.layout({ 200 | name: "dagre", 201 | nodeDimensionsIncludeLabels: true, 202 | padding: "10", 203 | animate: true, 204 | animationDuration: 1000, 205 | }); 206 | layout.run(); 207 | cy.zoomingEnabled(false); 208 | cy.on("click", (event) => { 209 | if ( 210 | event.target._private.data.class === "deployment" && 211 | event.target._private.data.label !== undefined && 212 | event.target._private.data.target === undefined && 213 | event.target._private.data.label !== "Kubernetes Cluster" && 214 | !namespacesArr.includes(event.target._private.data.label) 215 | ) { 216 | props.setView("Node View"); 217 | props.setNamespace(getNamespace(event.target._private.data.id)); 218 | props.setMasterNode(event.target._private.data.id); 219 | props.setTrigger(true); 220 | console.log(event.target._private.data.id); 221 | } 222 | }); 223 | cy.on("mouseover", "node[class = 'deployment']", function (event) { 224 | event.target.style("background-image", [ 225 | "https://i.ibb.co/crdrm4F/icons8-big-parcel-35-orange.png", 226 | ]); 227 | event.target.style("background-color", "rgb(230,74,0)"); 228 | event.target.style("border-color", "rgb(230,74,0)"); 229 | event.target.style("border-width", "1"); 230 | }); 231 | cy.on("mouseout", "node[class = 'deployment']", function (event) { 232 | event.target.style("background-image", [ 233 | "https://i.ibb.co/9V5KVmP/icons8-big-parcel-35.png", 234 | ]); 235 | event.target.style("background-color", "white"); 236 | event.target.style("border-width", "0"); 237 | }); 238 | return () => { 239 | console.log("cleanup"); 240 | }; 241 | }, [props.dataArray]); 242 | 243 | const limitSidebarHeight = 244 | document.getElementById("clusterView")?.style.height; 245 | 246 | return ( 247 |
248 |

{props.view}

249 |
250 |
251 |
255 |
256 | 260 |
261 |
262 | 263 |
264 | 265 |
266 |
267 |
272 |
273 |
274 |
275 | ); 276 | } 277 | 278 | export default ClusterView; -------------------------------------------------------------------------------- /src/scss/GraphStyles.ts: -------------------------------------------------------------------------------- 1 | import Cytoscape from 'cytoscape'; 2 | const kubernetesIcon = "https://image.pngaaa.com/630/3063630-middle.png" 3 | const textColor = "black" 4 | const backgroundColor = "" 5 | //#932af5 6 | const borderColor = "rgb(87,39,199)" 7 | //orange rgb(255, 136, 0) 8 | const arrowColor = "rgb(87,39,199)" 9 | const connectionColor = "rgb(75, 75, 75)" 10 | const font = "Calibri"; 11 | const podIcon = "https://i.ibb.co/VDG8MVh/podicon3.png" 12 | const labelColor = "whitesmoke"; 13 | const networkPolicyNodeSize = '10%'; 14 | const networkPolicyFontSize = "3%" 15 | 16 | export let GraphStyles:Cytoscape.Stylesheet[] = [ 17 | { 18 | selector: 'node', 19 | style: { 20 | shape: "ellipse", 21 | width: "40%", 22 | height: "40%", 23 | "font-size": "14%", 24 | "font-weight": "bold", 25 | "font-family": font, 26 | content: 'data(label)', 27 | "text-valign": "bottom", 28 | "text-halign": "center", 29 | "text-wrap": "wrap", 30 | "text-max-width": "140", 31 | "background-color": "whitesmoke", 32 | color: textColor, 33 | }, 34 | }, 35 | { 36 | selector: 'node[class = "daemonSet"]', 37 | style: { 38 | shape: "round-pentagon", 39 | width: "40%", 40 | height: "40%", 41 | "font-size": "14%", 42 | "font-weight": "bold", 43 | "font-family": font, 44 | content: 'data(label)', 45 | "text-valign": "bottom", 46 | "text-halign": "center", 47 | "text-wrap": "wrap", 48 | "text-max-width": "140", 49 | "background-color": "whitesmoke", 50 | color: "whitesmoke", 51 | }, 52 | }, 53 | { 54 | selector: 'node[class = "hover"]', 55 | style: { 56 | shape: "ellipse", 57 | width: "40%", 58 | height: "40%", 59 | "font-size": "14%", 60 | "font-weight": "bold", 61 | "font-family": font, 62 | content: 'data(label)', 63 | "text-valign": "bottom", 64 | "text-halign": "center", 65 | "text-wrap": "wrap", 66 | "text-max-width": "140", 67 | "background-color": "lightblue", 68 | "border-color": "black", 69 | "border-width": "2", 70 | color: "whitesmoke", 71 | 72 | }, 73 | }, 74 | { 75 | selector: 'node[class = "container"]', 76 | style: { 77 | shape: "ellipse", 78 | width: "42%", 79 | height: "42%", 80 | "font-size": "14%", 81 | "font-weight": "bold", 82 | "font-family": font, 83 | content: 'data(label)', 84 | "text-valign": "bottom", 85 | "text-halign": "center", 86 | "text-wrap": "wrap", 87 | "text-max-width": "140", 88 | "background-image": ["https://i.ibb.co/kgmDR1k/icons8-shipping-container-35.png"], 89 | color: "whitesmoke", 90 | }, 91 | }, 92 | { 93 | selector: 'node[class = "image"]', 94 | style: { 95 | shape: "ellipse", 96 | width: "42%", 97 | height: "42%", 98 | "font-size": "14%", 99 | "font-weight": "bold", 100 | "font-family": font, 101 | content: 'data(label)', 102 | "text-valign": "bottom", 103 | "text-halign": "center", 104 | "text-wrap": "wrap", 105 | "text-max-width": "140", 106 | "background-color": "whitesmoke", 107 | "background-image": ["https://i.ibb.co/0FHRyF1/icons8-file-35.png"], 108 | color: labelColor, 109 | }, 110 | }, 111 | { 112 | selector: 'node[class = "pod"]', 113 | style: { 114 | shape: "ellipse", 115 | width: "40%", 116 | height: "40%", 117 | "font-size": "14%", 118 | "font-weight": "bold", 119 | "font-family": font, 120 | content: 'data(label)', 121 | "text-valign": "bottom", 122 | "text-halign": "center", 123 | "text-wrap": "wrap", 124 | "text-max-width": "140", 125 | "background-image": [podIcon], 126 | color: labelColor, 127 | }, 128 | }, 129 | { 130 | selector: 'node[class = "deployment"]', 131 | style: { 132 | shape: "ellipse", 133 | width: "40%", 134 | height: "40%", 135 | "font-size": "14%", 136 | "font-weight": "bold", 137 | "font-family": font, 138 | content: 'data(label)', 139 | "text-valign": "bottom", 140 | "text-halign": "center", 141 | "text-wrap": "wrap", 142 | "text-max-width": "140", 143 | "background-color": "whitesmoke", 144 | "background-image": ["https://i.ibb.co/9V5KVmP/icons8-big-parcel-35.png"], 145 | color: labelColor, 146 | }, 147 | }, 148 | { 149 | selector: 'node[id = "Kubernetes Cluster"]', 150 | style: { 151 | shape: "round-heptagon", 152 | width: "80%", 153 | height: "80%", 154 | "font-size": "18", 155 | "font-family": font, 156 | content: 'data(label)', 157 | "text-valign": "center", 158 | "text-halign": "center", 159 | "text-wrap": "wrap", 160 | "text-max-width": "", 161 | "background-color": "whitesmoke", 162 | "border-color": borderColor, 163 | "border-width": "2", 164 | color: textColor, 165 | } 166 | }, 167 | { 168 | selector: 'node[class = "stateful"]', 169 | style: { 170 | shape: "ellipse", 171 | width: "60%", 172 | height: "60%", 173 | "font-size": "14%", 174 | "font-weight": "bold", 175 | "font-family": font, 176 | content: 'data(label)', 177 | "text-valign": "bottom", 178 | "text-halign": "center", 179 | "text-wrap": "wrap", 180 | "text-max-width": "10%", 181 | "background-image": ["https://i.ibb.co/82r4PV9/icons8-database-50.png"], 182 | "border-color": connectionColor, 183 | "border-width": "2", 184 | color: "whitesmoke", 185 | }, 186 | }, 187 | { 188 | selector: 'node[class = "service"]', 189 | style: { 190 | shape: "ellipse", 191 | width: "40%", 192 | height: "40%", 193 | "font-size": "14%", 194 | "font-weight": "bold", 195 | "font-family": font, 196 | content: 'data(label)', 197 | "text-valign": "bottom", 198 | "text-halign": "center", 199 | "text-wrap": "wrap", 200 | "text-max-width": "10%", 201 | "background-image": ["https://i.ibb.co/X5wCv7S/network-Icon.png"], 202 | "border-color": connectionColor, 203 | "border-width": "2", 204 | color: labelColor, 205 | }, 206 | }, 207 | { 208 | selector: 'node[class = "namespace"]', 209 | style: { 210 | shape: "round-rectangle", 211 | width: "150%", 212 | height: "50%", 213 | "font-size": "18", 214 | "font-weight": "bold", 215 | "font-family": font, 216 | content: 'data(label)', 217 | "text-valign": "center", 218 | "text-halign": "center", 219 | // "text-wrap": "wrap", 220 | "text-max-width": "140", 221 | "background-color": "whitesmoke", 222 | "border-color": borderColor, 223 | "border-width": "2", 224 | color: textColor, 225 | } 226 | }, 227 | { 228 | selector: 'node[class = "replicaSet"]', 229 | style: { 230 | shape: "ellipse", 231 | width: "75%", 232 | height: "75%", 233 | "font-size": "14%", 234 | "font-weight": "bold", 235 | "font-family": font, 236 | content: 'data(label)', 237 | "text-valign": "bottom", 238 | "text-halign": "center", 239 | "text-wrap": "wrap", 240 | "text-max-width": "10%", 241 | "background-image": ["https://i.ibb.co/FwR16DG/cubes64x64.png"], 242 | "border-color": connectionColor, 243 | "border-width": "2", 244 | color: "whitesmoke", 245 | }, 246 | }, 247 | { 248 | selector: 'node[class = "ingress"]', 249 | style: { 250 | shape: "round-rectangle", 251 | width: "40%", 252 | height: "10%", 253 | "font-size": networkPolicyFontSize, 254 | "font-weight": "bold", 255 | "font-family": font, 256 | content: 'data(label)', 257 | "text-valign": "center", 258 | "text-wrap": "wrap", 259 | "text-max-width": "40%", 260 | "background-color": "whitesmoke", 261 | "border-color": "black", 262 | "border-width": "1", 263 | color: textColor, 264 | }, 265 | }, 266 | { 267 | selector: 'node[class = "namespacePolicy"]', 268 | style: { 269 | shape: "rectangle", 270 | width: "50%", 271 | height: "10%", 272 | "text-max-width": "50%", 273 | "font-size": networkPolicyFontSize, 274 | "font-weight": "bold", 275 | "font-family": font, 276 | content: 'data(label)', 277 | "text-valign": "center", 278 | "text-wrap": "wrap", 279 | "background-color": "whitesmoke", 280 | "border-color": "green", 281 | "border-width": "1", 282 | color: textColor, 283 | }, 284 | }, 285 | { 286 | selector: 'node[class = "allowed"]', 287 | style: { 288 | shape: "triangle", 289 | width: networkPolicyNodeSize, 290 | height:networkPolicyNodeSize, 291 | "font-size": networkPolicyFontSize, 292 | "font-weight": "bold", 293 | "font-family": font, 294 | content: 'data(label)', 295 | "text-halign": "center", 296 | "text-valign": "bottom", 297 | "text-wrap": "wrap", 298 | "background-color": "whitesmoke", 299 | "border-color": "green", 300 | "border-width": "1", 301 | color: textColor, 302 | "text-background-color": "white", 303 | "text-background-opacity": 1, 304 | "text-background-shape": "roundrectangle", 305 | "text-background-padding": "1px", 306 | "text-margin-y": "3px" 307 | }, 308 | }, 309 | { 310 | selector: 'node[class = "egress"]', 311 | style: { 312 | shape: "triangle", 313 | width: networkPolicyNodeSize, 314 | height: networkPolicyNodeSize, 315 | "font-size": networkPolicyFontSize, 316 | "font-weight": "bold", 317 | "font-family": font, 318 | content: 'data(label)', 319 | "text-halign": "center", 320 | "text-valign": "bottom", 321 | "text-wrap": "wrap", 322 | "text-max-width": "140", 323 | "background-color": "whitesmoke", 324 | "border-color": "green", 325 | "border-width": "1", 326 | color: textColor, 327 | "text-background-color": "white", 328 | "text-background-opacity": 1, 329 | "text-background-shape": "roundrectangle", 330 | "text-background-padding": "1px", 331 | "text-margin-y": "3px" 332 | }, 333 | }, 334 | { 335 | selector: 'node[class = "except"]', 336 | style: { 337 | shape: "triangle", 338 | width: networkPolicyNodeSize, 339 | height: networkPolicyNodeSize, 340 | "font-size": networkPolicyFontSize, 341 | "font-family": font, 342 | content: 'data(label)', 343 | "text-halign": 'center', 344 | "text-valign": "bottom", 345 | "text-wrap": "wrap", 346 | "background-color": "whitesmoke", 347 | "border-color": "red", 348 | "border-width": "1", 349 | color: textColor, 350 | "text-background-color": "white", 351 | "text-background-opacity": 1, 352 | "text-background-shape": "roundrectangle", 353 | "text-background-padding": "1px", 354 | "text-margin-y": "3px" 355 | }, 356 | }, 357 | { 358 | selector: 'edge', 359 | style: { 360 | "curve-style": "bezier", 361 | color: "blue", 362 | "text-background-color": "#ffffff", 363 | "text-background-opacity": 1, 364 | "text-background-padding": "3", 365 | width: "3", 366 | "target-arrow-shape": "triangle", 367 | "line-color": arrowColor, 368 | "target-arrow-color": arrowColor, 369 | "font-weight": "bold", 370 | }, 371 | }, 372 | { 373 | selector: 'edge[label = "allowed"]', 374 | style: { 375 | "curve-style": "bezier", 376 | color: "blue", 377 | "text-background-color": "#ffffff", 378 | "text-background-opacity": 1, 379 | "text-background-padding": "3", 380 | width: "1", 381 | "target-arrow-shape": "triangle", 382 | "line-color": "green", 383 | "target-arrow-color": "green", 384 | "arrow-scale": 0.5, 385 | }, 386 | }, 387 | { 388 | selector: 'edge[label = "except"]', 389 | style: { 390 | "curve-style": "bezier", 391 | color: "blue", 392 | "text-background-color": "#ffffff", 393 | "text-background-opacity": 1, 394 | "text-background-padding": "3", 395 | width: "1", 396 | "target-arrow-shape": "triangle", 397 | "line-color": "red", 398 | "target-arrow-color": "red", 399 | "arrow-scale": 0.5, 400 | "font-weight": "bold", 401 | }, 402 | }, 403 | { 404 | selector: 'edge[label = "stateful"]', 405 | style: { 406 | "curve-style": "bezier", 407 | color: "blue", 408 | "text-background-color": "#ffffff", 409 | "text-background-opacity": 1, 410 | "text-background-padding": "3", 411 | width: "3", 412 | "target-arrow-shape": "triangle", 413 | "line-color": "orange", 414 | "target-arrow-color": "orange", 415 | "font-weight": "bold", 416 | "line-style": 'dashed' 417 | }, 418 | }, 419 | { 420 | selector: 'edge[label = "connection"]', 421 | style: { 422 | "curve-style": "bezier", 423 | color: "blue", 424 | "text-background-color": "#ffffff", 425 | "text-background-opacity": 1, 426 | "text-background-padding": "3", 427 | width: "3", 428 | "target-arrow-shape": "none", 429 | "line-color": connectionColor, 430 | "font-weight": "bold", 431 | "line-style": 'dashed' 432 | }, 433 | }, 434 | { 435 | selector: 'edge[label = "service"]', 436 | style: { 437 | "curve-style": "bezier", 438 | color: "blue", 439 | "text-background-color": "#ffffff", 440 | "text-background-opacity": 1, 441 | "text-background-padding": "3", 442 | width: "3", 443 | "target-arrow-shape": "triangle", 444 | "line-color": arrowColor, 445 | "target-arrow-color": arrowColor, 446 | "font-weight": "bold", 447 | }, 448 | }, 449 | { 450 | selector: 'edge[label = "daemonSet"]', 451 | style: { 452 | "curve-style": "bezier", 453 | color: "blue", 454 | "text-background-color": "#ffffff", 455 | "text-background-opacity": 1, 456 | "text-background-padding": "3", 457 | width: "2", 458 | "target-arrow-shape": "triangle", 459 | "target-arrow-color": "lightblue", 460 | "line-color": "lightblue", 461 | "font-weight": "bold", 462 | }, 463 | }, 464 | ] --------------------------------------------------------------------------------