├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── images │ ├── cluster-connect.gif │ ├── dashboard-screenshot.png │ ├── electron-react-webpack-boilerplate.png │ ├── nodes-screenshot.png │ ├── pods-to-podview.gif │ └── podview-screenshot.png ├── gitWorkflow.md ├── grafana-kr8s-dashboard.json ├── kr8sserver ├── .dockerignore ├── Dockerfile ├── controllers │ └── k8sController.js ├── package-lock.json ├── package.json └── server.js ├── main.js ├── manifests ├── clusterRole.yaml ├── config-map.yaml ├── grafana-datasource-config.yaml ├── grafana-depl.yaml ├── grafana-srv.yaml ├── kr8sserver-depl.yaml ├── ks-metrics-cr.yaml ├── ks-metrics-crbind.yaml ├── ks-metrics-sa.yaml ├── ks-metrics-srv.yaml ├── ksmetrics-depl.yaml ├── node-exporter-dset.yaml └── prometheus-depl.yaml ├── package-lock.json ├── package.json ├── postcss.config.js ├── provisioning └── dashboards │ ├── cluster.json │ └── default.yaml ├── src ├── APIcalls.js ├── __tests__ │ ├── Banner.test.jsx │ ├── Dashboard.test.jsx │ ├── List.test.jsx │ ├── Nodes.test.jsx │ ├── PodView.test.jsx │ ├── Pods.test.jsx │ └── supertest.js ├── assets │ └── css │ │ ├── App.module.css │ │ ├── Banner.module.css │ │ ├── ClusterConnect.module.css │ │ ├── Dashboard.module.css │ │ ├── Header.module.css │ │ ├── LineGraph.module.css │ │ ├── List.module.css │ │ ├── NodeView.module.css │ │ ├── Nodes.module.css │ │ ├── PodView.module.css │ │ ├── Pods.module.css │ │ ├── Sidebar.module.css │ │ ├── Speedometer.module.css │ │ ├── Tile.module.css │ │ ├── _example │ │ └── _example.css │ │ └── imgs │ │ ├── KR8S-Background.png │ │ ├── KR8S-PNG-1600-900.png │ │ ├── Kr8s2.jpg │ │ ├── Transparent_Image_3.png │ │ ├── icon-24@256.png │ │ ├── kr8s-connect.svg │ │ ├── kr8s-text.svg │ │ ├── kr8s-wheel.svg │ │ ├── kr8s.ico │ │ ├── preview_1_450x120.jpg │ │ ├── preview_2_280x180.jpg │ │ └── under-construction.png ├── components │ ├── Banner.jsx │ ├── ClusterConnect.jsx │ ├── Header.jsx │ ├── LineGraph.jsx │ ├── List.jsx │ ├── NodeView.jsx │ ├── PodView.jsx │ ├── Sidebar.jsx │ ├── Speedometer.jsx │ └── Tile.jsx ├── containers │ ├── App.jsx │ ├── Dashboard.jsx │ ├── Nodes.jsx │ └── Pods.jsx ├── index.html ├── index.js └── setupTests.js ├── webpack.build.config.js ├── webpack.dev.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = crlf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/test", "**/__tests__"], 4 | "env": { 5 | "node": true, 6 | "browser": true, 7 | "es2021": true 8 | }, 9 | "plugins": ["react"], 10 | "extends": ["eslint:recommended", "plugin:react/recommended"], 11 | "parserOptions": { 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "rules": { 18 | "indent": ["warn", 2], 19 | "no-unused-vars": ["off", { "vars": "local" }], 20 | "no-case-declarations": "off", 21 | "prefer-const": "warn", 22 | "quotes": ["warn", "single"], 23 | "react/prop-types": "off", 24 | "semi": ["warn", "always"], 25 | "space-infix-ops": "warn" 26 | }, 27 | "settings": { 28 | "react": { "version": "detect"} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build folder and files # 2 | ########################## 3 | *builds/ 4 | 5 | # Development folders and files # 6 | ################################# 7 | .tmp/ 8 | dist/ 9 | node_modules/ 10 | *.compiled.* 11 | package-lock.json 12 | yarn.lock 13 | 14 | # Folder config file # 15 | ###################### 16 | Desktop.ini 17 | 18 | # Folder notes # 19 | ################ 20 | _ignore/ 21 | 22 | # Log files & folders # 23 | ####################### 24 | logs/ 25 | *.log 26 | npm-debug.log* 27 | .npm 28 | 29 | # Packages # 30 | ############ 31 | # it's better to unpack these files and commit the raw source 32 | # git has its own built in compression methods 33 | *.7z 34 | *.dmg 35 | *.gz 36 | *.iso 37 | *.jar 38 | *.rar 39 | *.tar 40 | *.zip 41 | 42 | # Photoshop & Illustrator files # 43 | ################################# 44 | *.ai 45 | *.eps 46 | *.psd 47 | 48 | # Windows & Mac file caches # 49 | ############################# 50 | .DS_Store 51 | Thumbs.db 52 | ehthumbs.db 53 | 54 | # Windows shortcuts # 55 | ##################### 56 | *.lnk 57 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _ignore/ 2 | docs/ 3 | builds/ 4 | dist/ 5 | .editorconfig 6 | code-of-conduct.md 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Kr8sDevelopers@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 3 | 4 | Please note we have a [Code of Conduct](https://github.com/oslabs-beta/kr8s/blob/dev/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 5 | 6 | # Pull Request Process 7 | Ensure any install or build dependencies are removed before the end of the layer when doing a build. 8 | Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 9 | Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is SemVer. 10 | You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Alex Devero (alexdevero.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kr8s 2 | 3 |

4 | 5 |

6 | 7 | last commit 8 | license 9 | github stars 10 | twitter 11 |

12 | 13 | ## Table of Contents 14 | 15 | - [Getting Started](#getting-started) 16 | - [Features](#features) 17 | - [Upcoming Features](#upcoming-features) 18 | - [Links](#links) 19 | - [Meet Our Team](#meet-our-team) 20 |
21 |
22 | 23 | ## Summary 24 | 25 | [Kr8s](https://kr8s.dev/) is a desktop application made for developers that need to monitor and visualize their Kubernetes clusters in a user friendly GUI. This easy-to-use tool will display the most important metrics for your cluster, nodes, pods, and containers. Kr8s seamlessly implements Prometheus and Grafana to give you everything you need in one integrated application. 26 |
27 |
28 | 29 | # Getting Started 30 | 31 | ## Prerequisites 32 | 33 | Kr8s functionality assumes that you have Docker and Kubernetes already installed and running on your machine. The simplest way to install both is to follow the instructions for [Docker Desktop](https://www.docker.com/get-started) installation, then enable Kubernetes from the Settings menu. 34 |
35 |
36 | 37 | ## Install Kr8s 38 | 39 | To install Kr8s, you will need to first clone this repository to your local machine. Then in the root directory, run the following commands and a desktop application will launch: 40 | 41 | ``` 42 | npm install 43 | ``` 44 | 45 | ``` 46 | npm run start 47 | ``` 48 | 49 |
50 | 51 | ## I don't have an existing cluster but I want to demo Kr8s 52 | 53 | If you are interested in trying out Kr8s, but do not yet have a running Kubernetes cluster, we created a rudimentary microservices application you can use to test features. Simply clone the [kr8sdemo repository](https://github.com/oslabs-beta/kr8sdemo) and follow the instruction in the README. 54 | 55 |
56 |
57 | 58 | ## Open App and Connect to Cluster 59 | 60 | The app opens to the connect page. Select “Local Cluster” to connect to your local Kubernetes context (make sure Kubernetes is running). During start up Kr8s will deploy its pre-configured monitoring services to your cluster automatically. Prometheus and Grafana instances will be created or re-configured if they already exist. After a successful connection, you are redirected to the Dashboard page where data immediately populates into the visualizers and refreshes on a 15 second interval. 61 |
62 |
63 | 64 |

65 |
66 |
67 | 68 | # Features 69 | 70 | ## Dashboard 71 | 72 | The Dashboard page is the first page you will see once you connect to your local cluster. This page will give you metrics of your cluster on a high-level including pod usage, CPU usage, and memory usage. 73 |
74 |
75 | 76 |

77 |
78 | 79 | ## Nodes 80 | 81 | The Nodes page will give you everything you will need to know about the nodes in your cluster such as the names and health of each node. 82 |
83 |
84 | 85 |

86 |
87 | 88 | ## Pods 89 | 90 | The Pods page will display the most important metrics you will need when monitoring all of your pods including pod CPU usage, memory usage, and the names and health of each pod. The Pod View page can be accessed when you click on a specific pod on the Pods page, displaying information specific to the selected pod and its containers. 91 |
92 |
93 | 94 |

95 |
96 | 97 |
98 |
99 | 100 | # Upcoming Features 101 | 102 | Kr8s is an open-source product in active development. Below are some features that the Kr8s team plans to implement in upcoming versions. 103 | 104 | - Support for multiple cluster connections 105 | - Cloud service integration 106 | - Custom visualizer creation 107 | - Cluster object management and deployment 108 | 109 | The team is always open to feedback and collaborators, so If you are interested in contributing to Kr8s please refer to [CONTRIBUTING.md](https://github.com/oslabs-beta/kr8s/blob/dev/CONTRIBUTING.md) for submission guidelines! 110 |
111 |
112 | 113 | # Links 114 | 115 | - [Website](http://www.kr8s.dev/) 116 | - [LinkedIn](https://www.linkedin.com/company/kr8s/) 117 | - [Medium](https://medium.com/@kr8sdevelopers) 118 | - [Twitter](https://twitter.com/Kr8s4K) 119 | - [Product Hunt](https://www.producthunt.com/posts/kr8s-for-kubernetes) 120 |
121 |
122 | 123 | # Meet Our Team 124 | 125 | Adam Sheff 126 | [LinkedIn](https://www.linkedin.com/in/adam-sheff/) | [Github](https://github.com/adamISheff) 127 | 128 | Duke Lee 129 | [LinkedIn](https://www.linkedin.com/in/duke-lee) | [Github](https://github.com/dukelee11) 130 | 131 | Justin Stoddard 132 | [LinkedIn](https://www.linkedin.com/in/jgstoddard/) | [Github](https://github.com/jgstoddard) 133 | 134 | Reland Boyle 135 | [LinkedIn](https://www.linkedin.com/in/relandboyle/) | [Github](https://github.com/GlorifiedBicycle) 136 | -------------------------------------------------------------------------------- /docs/images/cluster-connect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/cluster-connect.gif -------------------------------------------------------------------------------- /docs/images/dashboard-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/dashboard-screenshot.png -------------------------------------------------------------------------------- /docs/images/electron-react-webpack-boilerplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/electron-react-webpack-boilerplate.png -------------------------------------------------------------------------------- /docs/images/nodes-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/nodes-screenshot.png -------------------------------------------------------------------------------- /docs/images/pods-to-podview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/pods-to-podview.gif -------------------------------------------------------------------------------- /docs/images/podview-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/docs/images/podview-screenshot.png -------------------------------------------------------------------------------- /gitWorkflow.md: -------------------------------------------------------------------------------- 1 | ## **GIT WORKFLOWS** 2 | ### To start working on a *new* feature: 3 | 1. create a new issue with an appropriate name, add it to current project, and assign it to yourself (or appropriate person) 4 | 2. create a local feature branch with corresponding name, set its upstream to a branch with the same name in github 5 | 6 | ### **To merge dev branch into your local branch** 7 | *Do this whenever there has been an update to dev* **and BEFORE** *pushing your local changes on your feature branch to the remote branch...*: 8 | 1. make sure you're on your feature branch (confirm with `git branch`) 9 | 2. `git commit` your recent changes (**but don't push yet!**) 10 | 3. `git checkout dev` to switch to dev branch 11 | 4. `git pull origin dev` to pull down most recent changes to your local dev branch 12 | 5. `git checkout ` to switch back to your local feature branch 13 | 6. `git merge dev` to merge newest changes from dev into your local branch 14 | 7. `git push origin ` to update the remote feature branch to include your local changes and dev's changes 15 | 16 | ### **To merge a feature branch into dev** 17 | *Double check you have merged most current version of dev into your local feature branch *AND* you've pushed your local feature branch changes to the remote feature branch* 18 | 1. Go to the GitHub repo 19 | 2. If you recently `pushed` to your feature branch, there will be an alert at the top of the page that says: `" had recent pushes x minutes ago"` with a button that says `Compare & pull request`. Click the button. 20 | 3. Make sure `base: dev` and `compare: featurebranchname` 21 | 4. Add succinct commentary about what changes are included. 22 | 5. Click `Create Pull Request` 23 | 6. Let Scrum Master know that you submitted a PR that needs review. 24 | -------------------------------------------------------------------------------- /kr8sserver/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /kr8sserver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN npm install 6 | COPY ./ ./ 7 | 8 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /kr8sserver/controllers/k8sController.js: -------------------------------------------------------------------------------- 1 | const k8s = require('@kubernetes/client-node'); 2 | const kc = new k8s.KubeConfig(); 3 | kc.loadFromDefault(); 4 | 5 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 6 | const k8sApi2 = kc.makeApiClient(k8s.ExtensionsV1beta1Api); 7 | const k8sApi3 = kc.makeApiClient(k8s.AppsV1Api); 8 | 9 | 10 | const k8sController = {}; 11 | 12 | 13 | //// All API middleware for 'default' Namespace //// 14 | 15 | k8sController.getNamespaceList = (req, res, next) => { 16 | k8sApi.listNamespace() 17 | .then((data) => { 18 | console.log(data.body); 19 | res.locals.namespaceList = data.body; 20 | return next(); 21 | }) 22 | .catch((err) => { 23 | return next({ 24 | log: `ERROR in k8sController.getNamespaceList: ${err} `, 25 | status: 500, 26 | message: 'Error occurred: Could not retrieve namespace list' 27 | }); 28 | }); 29 | }; 30 | 31 | k8sController.getPodList = (req, res, next) => { 32 | k8sApi.listNamespacedPod('default') 33 | .then((data) => { 34 | console.log(data.body); 35 | res.locals.podList = data.body; 36 | return next(); 37 | }) 38 | .catch((err) => { 39 | return next({ 40 | log: `ERROR in k8sController.getPodList: ${err} `, 41 | status: 500, 42 | message: 'Error occurred: Could not retrieve pod list' 43 | }); 44 | }); 45 | }; 46 | 47 | k8sController.getNodeList = (req, res, next) => { 48 | k8sApi.listNode('default') 49 | .then((data) => { 50 | console.log(data.body); 51 | res.locals.nodeList = data.body; 52 | return next(); 53 | }) 54 | .catch((err) => { 55 | return next({ 56 | log: `ERROR in k8sController.getNodeList: ${err} `, 57 | status: 500, 58 | message: 'Error occurred: Could not retrieve node list' 59 | }); 60 | }); 61 | }; 62 | 63 | k8sController.getDeploymentList = (req, res, next) => { 64 | k8sApi3.listNamespacedDeployment('default') 65 | .then((data) =>{ 66 | console.log(data.body); 67 | res.locals.deploymentList = data.body; 68 | return next(); 69 | }) 70 | .catch((err) => { 71 | return next({ 72 | log: `ERROR in k8sController.getDeploymentList: ${err} `, 73 | status: 500, 74 | message: 'Error occurred: Could not retrieve node list' 75 | }); 76 | }); 77 | }; 78 | 79 | k8sController.getServiceList = (req, res, next) => { 80 | k8sApi.listNamespacedService('default') 81 | .then((data) =>{ 82 | console.log(data.body); 83 | res.locals.serviceList = data.body; 84 | return next(); 85 | }) 86 | .catch((err) => { 87 | return next({ 88 | log: `ERROR in k8sController.getServiceList: ${err} `, 89 | status: 500, 90 | message: 'Error occurred: Could not retrieve service list' 91 | }); 92 | }); 93 | }; 94 | 95 | k8sController.getIngressList = (req, res, next) => { 96 | k8sApi2.listNamespacedIngress('default') 97 | .then((data) =>{ 98 | console.log(data.body); 99 | res.locals.ingressList = data.body; 100 | return next(); 101 | }) 102 | .catch((err) => { 103 | return next({ 104 | log: `ERROR in k8sController.getIngressList: ${err} `, 105 | status: 500, 106 | message: 'Error occurred: Could not retrieve ingress list' 107 | }); 108 | }); 109 | }; 110 | 111 | module.exports = k8sController; 112 | -------------------------------------------------------------------------------- /kr8sserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@kubernetes/client-node": "^0.15.1", 15 | "express": "^4.17.1", 16 | "nodemon": "^2.0.14" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /kr8sserver/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const k8sController = require('./controllers/k8sController.js') 3 | 4 | 5 | const PORT = 3400; 6 | 7 | app = express(); 8 | 9 | app.use(express.json()); 10 | app.use(express.urlencoded({ extended: true })); 11 | 12 | 13 | /* 14 | Routes handling requests for k8s cluster info from Prometheus 15 | Middleware will retrieve requested information from Prometheus 16 | */ 17 | 18 | app.get('/api/namespaceList', 19 | k8sController.getPodList, 20 | (req, res) => res.status(200).json(res.locals.namespaceList)); 21 | 22 | app.get('/api/podList', 23 | k8sController.getPodList, 24 | (req, res) => res.status(200).json(res.locals.podList)); 25 | 26 | app.get('/api/nodeList', 27 | k8sController.getNodeList, 28 | (req, res) => res.status(200).json(res.locals.nodeList)); 29 | 30 | app.get('/api/deploymentList', 31 | k8sController.getDeploymentList, 32 | (req, res) => res.status(200).json(res.locals.deploymentList)); 33 | 34 | app.get('/api/serviceList', 35 | k8sController.getServiceList, 36 | (req, res) => res.status(200).json(res.locals.serviceList)); 37 | 38 | app.get('/api/ingressList', 39 | k8sController.getIngressList, 40 | (req, res) => res.status(200).json(res.locals.ingressList)); 41 | 42 | 43 | 44 | //Global error handler 45 | app.use((err, req, res, next) => { 46 | console.error(err); 47 | const defaultClientError = { 48 | status: 500, 49 | message: { error: 'An unexpected error occurred.' }, 50 | }; 51 | const errorObj = Object.assign(defaultClientError, err); 52 | return res.status(errorObj.status).json(errorObj.message); 53 | }); 54 | 55 | 56 | app.listen(PORT, () => console.log(`Server listening on port ${PORT} `)); 57 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { app, BrowserWindow } = require('electron') 4 | const path = require('path') 5 | const url = require('url') 6 | const { exec } = require('child_process') 7 | 8 | // Keep a global reference of the window object, if you don't, the window will 9 | // be closed automatically when the JavaScript object is garbage collected. 10 | let mainWindow; 11 | 12 | // Keep a reference for dev mode 13 | let dev = false 14 | 15 | 16 | const kubeCommand = 'kubectl create namespace monitoring'; 17 | const kubeCommand2 = 'kubectl apply -f manifests' 18 | 19 | // Run node command to create monitoring namespace for metric scraping 20 | exec(kubeCommand, (error, stdout, stderr) => { 21 | if (error) { 22 | console.error(`error: ${error.message}`); 23 | return; 24 | } 25 | 26 | if (stderr) { 27 | console.error(`stderr: ${stderr}`); 28 | return; 29 | } 30 | 31 | console.log(`stdout:\n${stdout}`); 32 | }); 33 | 34 | // Run node command to apply all yaml files in manifests 35 | exec(kubeCommand2, (error, stdout, stderr) => { 36 | if (error) { 37 | console.error(`error: ${error.message}`); 38 | return; 39 | } 40 | 41 | if (stderr) { 42 | console.error(`stderr: ${stderr}`); 43 | return; 44 | } 45 | 46 | console.log(`stdout:\n${stdout}`); 47 | }); 48 | 49 | if (process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'development') { 50 | dev = true; 51 | } 52 | 53 | // Temporary fix broken high-dpi scale factor on Windows (125% scaling) 54 | // info: https://github.com/electron/electron/issues/9691 55 | if (process.platform === 'win32') { 56 | app.commandLine.appendSwitch('high-dpi-support', 'true'); 57 | app.commandLine.appendSwitch('force-device-scale-factor', '1'); 58 | } 59 | 60 | function createWindow() { 61 | // Create the browser window. 62 | mainWindow = new BrowserWindow({ 63 | width: 1440, 64 | height: 900, 65 | x: 0, 66 | y: 0, 67 | icon: path.resolve(__dirname, '/kr8s.ico'), 68 | resizable: false, 69 | show: false, 70 | webPreferences: { 71 | nodeIntegration: true, 72 | contextIsolation: false 73 | } 74 | }) 75 | 76 | // and load the index.html of the app. 77 | let indexPath; 78 | let fileLoader = () => {}; 79 | 80 | if (dev && process.argv.indexOf('--noDevServer') === -1) { 81 | indexPath = 'http://localhost:8080/index.html'; 82 | fileLoader = mainWindow.loadURL(indexPath); 83 | } else { 84 | indexPath = path.join(process.cwd(), 'dist', 'index.html'); 85 | fileLoader = mainWindow.loadFile(indexPath); 86 | } 87 | 88 | console.log('*** indexPath: ', indexPath); 89 | () => fileLoader; 90 | 91 | // mainWindow.webContents.openDevTools(); 92 | 93 | // Don't show until we are ready and loaded 94 | mainWindow.once('ready-to-show', () => { 95 | mainWindow.show(); 96 | 97 | // Open the DevTools automatically if developing 98 | // if (dev) { 99 | // const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer') 100 | 101 | // installExtension(REACT_DEVELOPER_TOOLS) 102 | // .catch(err => console.log('Error loading React DevTools: ', err)) 103 | // mainWindow.webContents.openDevTools() 104 | // } 105 | }) 106 | 107 | // Emitted when the window is closed. 108 | mainWindow.on('closed', function() { 109 | // Dereference the window object, usually you would store windows 110 | // in an array if your app supports multi windows, this is the time 111 | // when you should delete the corresponding element. 112 | mainWindow = null; 113 | }) 114 | } 115 | 116 | // This method will be called when Electron has finished 117 | // initialization and is ready to create browser windows. 118 | // Some APIs can only be used after this event occurs. 119 | app.on('ready', createWindow); 120 | 121 | // Quit when all windows are closed. 122 | app.on('window-all-closed', () => { 123 | // On macOS it is common for applications and their menu bar 124 | // to stay active until the user quits explicitly with Cmd + Q 125 | if (process.platform !== 'darwin') { 126 | app.quit(); 127 | } 128 | }) 129 | 130 | app.on('activate', () => { 131 | // On macOS it's common to re-create a window in the app when the 132 | // dock icon is clicked and there are no other windows open. 133 | if (mainWindow === null) { 134 | createWindow(); 135 | } 136 | }) 137 | -------------------------------------------------------------------------------- /manifests/clusterRole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: prometheus 5 | rules: 6 | - apiGroups: [""] 7 | resources: 8 | - nodes 9 | - nodes/proxy 10 | - services 11 | - endpoints 12 | - pods 13 | verbs: ["get", "list", "watch"] 14 | - apiGroups: 15 | - extensions 16 | resources: 17 | - ingresses 18 | verbs: ["get", "list", "watch"] 19 | - nonResourceURLs: ["/metrics"] 20 | verbs: ["get"] 21 | --- 22 | apiVersion: rbac.authorization.k8s.io/v1 23 | kind: ClusterRoleBinding 24 | metadata: 25 | name: prometheus 26 | roleRef: 27 | apiGroup: rbac.authorization.k8s.io 28 | kind: ClusterRole 29 | name: prometheus 30 | subjects: 31 | - kind: ServiceAccount 32 | name: default 33 | namespace: monitoring 34 | -------------------------------------------------------------------------------- /manifests/config-map.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: prometheus-server-conf 5 | labels: 6 | name: prometheus-server-conf 7 | namespace: monitoring 8 | data: 9 | prometheus.rules: |- 10 | groups: 11 | - name: devopscube demo alert 12 | rules: 13 | - alert: High Pod Memory 14 | expr: sum(container_memory_usage_bytes) > 1 15 | for: 1m 16 | labels: 17 | severity: slack 18 | annotations: 19 | summary: High Memory Usage 20 | prometheus.yml: |- 21 | global: 22 | scrape_interval: 5s 23 | evaluation_interval: 5s 24 | rule_files: 25 | - /etc/prometheus/prometheus.rules 26 | alerting: 27 | alertmanagers: 28 | - scheme: http 29 | static_configs: 30 | - targets: 31 | - "alertmanager.monitoring.svc:9093" 32 | 33 | scrape_configs: 34 | - job_name: 'node-exporter' 35 | static_configs: 36 | - targets: ['127.0.0.1:9100'] 37 | kubernetes_sd_configs: 38 | - role: endpoints 39 | relabel_configs: 40 | - source_labels: [__meta_kubernetes_endpoints_name] 41 | regex: 'node-exporter' 42 | action: keep 43 | 44 | - job_name: 'kubernetes-apiservers' 45 | 46 | kubernetes_sd_configs: 47 | - role: endpoints 48 | scheme: https 49 | 50 | tls_config: 51 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 52 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 53 | 54 | relabel_configs: 55 | - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] 56 | action: keep 57 | regex: default;kubernetes;https 58 | 59 | - job_name: 'kubernetes-nodes' 60 | 61 | scheme: https 62 | 63 | tls_config: 64 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 65 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 66 | 67 | kubernetes_sd_configs: 68 | - role: node 69 | 70 | relabel_configs: 71 | - action: labelmap 72 | regex: __meta_kubernetes_node_label_(.+) 73 | - target_label: __address__ 74 | replacement: kubernetes.default.svc:443 75 | - source_labels: [__meta_kubernetes_node_name] 76 | regex: (.+) 77 | target_label: __metrics_path__ 78 | replacement: /api/v1/nodes/${1}/proxy/metrics 79 | 80 | - job_name: 'kubernetes-pods' 81 | 82 | kubernetes_sd_configs: 83 | - role: pod 84 | 85 | relabel_configs: 86 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] 87 | action: keep 88 | regex: true 89 | - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] 90 | action: replace 91 | target_label: __metrics_path__ 92 | regex: (.+) 93 | - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] 94 | action: replace 95 | regex: ([^:]+)(?::\d+)?;(\d+) 96 | replacement: $1:$2 97 | target_label: __address__ 98 | - action: labelmap 99 | regex: __meta_kubernetes_pod_label_(.+) 100 | - source_labels: [__meta_kubernetes_namespace] 101 | action: replace 102 | target_label: kubernetes_namespace 103 | - source_labels: [__meta_kubernetes_pod_name] 104 | action: replace 105 | target_label: kubernetes_pod_name 106 | 107 | - job_name: 'kube-state-metrics' 108 | static_configs: 109 | - targets: ['kube-state-metrics.kube-system.svc.cluster.local:8080'] 110 | 111 | - job_name: 'kubernetes-cadvisor' 112 | 113 | scheme: https 114 | 115 | tls_config: 116 | ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 117 | bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token 118 | 119 | kubernetes_sd_configs: 120 | - role: node 121 | 122 | relabel_configs: 123 | - action: labelmap 124 | regex: __meta_kubernetes_node_label_(.+) 125 | - target_label: __address__ 126 | replacement: kubernetes.default.svc:443 127 | - source_labels: [__meta_kubernetes_node_name] 128 | regex: (.+) 129 | target_label: __metrics_path__ 130 | replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor 131 | 132 | - job_name: 'kubernetes-service-endpoints' 133 | 134 | kubernetes_sd_configs: 135 | - role: endpoints 136 | 137 | relabel_configs: 138 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] 139 | action: keep 140 | regex: true 141 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] 142 | action: replace 143 | target_label: __scheme__ 144 | regex: (https?) 145 | - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] 146 | action: replace 147 | target_label: __metrics_path__ 148 | regex: (.+) 149 | - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] 150 | action: replace 151 | target_label: __address__ 152 | regex: ([^:]+)(?::\d+)?;(\d+) 153 | replacement: $1:$2 154 | - action: labelmap 155 | regex: __meta_kubernetes_service_label_(.+) 156 | - source_labels: [__meta_kubernetes_namespace] 157 | action: replace 158 | target_label: kubernetes_namespace 159 | - source_labels: [__meta_kubernetes_service_name] 160 | action: replace 161 | target_label: kubernetes_name 162 | -------------------------------------------------------------------------------- /manifests/grafana-datasource-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: grafana-datasources 5 | namespace: monitoring 6 | data: 7 | prometheus.yaml: |- 8 | { 9 | "apiVersion": 1, 10 | "datasources": [ 11 | { 12 | "access":"proxy", 13 | "editable": true, 14 | "name": "prometheus", 15 | "orgId": 1, 16 | "type": "prometheus", 17 | "url": "http://prometheus-service.monitoring.svc:8080", 18 | "version": 1 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /manifests/grafana-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: grafana 5 | namespace: monitoring 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: grafana 11 | template: 12 | metadata: 13 | name: grafana 14 | labels: 15 | app: grafana 16 | spec: 17 | containers: 18 | - name: grafana 19 | image: grafana/grafana:latest 20 | ports: 21 | - name: grafana 22 | containerPort: 3000 23 | resources: 24 | limits: 25 | memory: "1Gi" 26 | cpu: "1000m" 27 | requests: 28 | memory: 500M 29 | cpu: "500m" 30 | volumeMounts: 31 | - mountPath: /var/lib/grafana 32 | name: grafana-storage 33 | - mountPath: /etc/grafana/provisioning/datasources 34 | name: grafana-datasources 35 | readOnly: false 36 | env: 37 | - name: GF_SECURITY_ALLOW_EMBEDDING 38 | value: "true" 39 | - name: GF_AUTH_ANONYMOUS_ENABLED 40 | value: "true" 41 | - name: GF_AUTH_ANONYMOUS_ORG_ROLE 42 | value: "Admin" 43 | volumes: 44 | - name: grafana-storage 45 | emptyDir: {} 46 | - name: grafana-datasources 47 | configMap: 48 | defaultMode: 420 49 | name: grafana-datasources 50 | -------------------------------------------------------------------------------- /manifests/grafana-srv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: grafana 5 | namespace: monitoring 6 | annotations: 7 | prometheus.io/scrape: 'true' 8 | prometheus.io/port: '3000' 9 | spec: 10 | selector: 11 | app: grafana 12 | type: NodePort 13 | ports: 14 | - port: 3000 15 | targetPort: 3000 16 | nodePort: 32000 -------------------------------------------------------------------------------- /manifests/kr8sserver-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kr8sserver-depl 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: kr8sserver 10 | template: 11 | metadata: 12 | labels: 13 | app: kr8sserver 14 | spec: 15 | containers: 16 | - name: kr8sserver 17 | image: kr8sserver/server 18 | --- 19 | apiVersion: v1 20 | kind: Service 21 | metadata: 22 | name: kr8sserver-np-srv 23 | spec: 24 | type: NodePort 25 | selector: 26 | app: kr8sserver 27 | ports: 28 | - name: kr8sserver 29 | protocol: TCP 30 | port: 3500 31 | targetPort: 3400 32 | nodePort: 31000 -------------------------------------------------------------------------------- /manifests/ks-metrics-cr.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kube-state-metrics 6 | app.kubernetes.io/version: v1.8.0 7 | name: kube-state-metrics 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | - secrets 14 | - nodes 15 | - pods 16 | - services 17 | - resourcequotas 18 | - replicationcontrollers 19 | - limitranges 20 | - persistentvolumeclaims 21 | - persistentvolumes 22 | - namespaces 23 | - endpoints 24 | verbs: 25 | - list 26 | - watch 27 | - apiGroups: 28 | - extensions 29 | resources: 30 | - daemonsets 31 | - deployments 32 | - replicasets 33 | - ingresses 34 | verbs: 35 | - list 36 | - watch 37 | - apiGroups: 38 | - apps 39 | resources: 40 | - statefulsets 41 | - daemonsets 42 | - deployments 43 | - replicasets 44 | verbs: 45 | - list 46 | - watch 47 | - apiGroups: 48 | - batch 49 | resources: 50 | - cronjobs 51 | - jobs 52 | verbs: 53 | - list 54 | - watch 55 | - apiGroups: 56 | - autoscaling 57 | resources: 58 | - horizontalpodautoscalers 59 | verbs: 60 | - list 61 | - watch 62 | - apiGroups: 63 | - authentication.k8s.io 64 | resources: 65 | - tokenreviews 66 | verbs: 67 | - create 68 | - apiGroups: 69 | - authorization.k8s.io 70 | resources: 71 | - subjectaccessreviews 72 | verbs: 73 | - create 74 | - apiGroups: 75 | - policy 76 | resources: 77 | - poddisruptionbudgets 78 | verbs: 79 | - list 80 | - watch 81 | - apiGroups: 82 | - certificates.k8s.io 83 | resources: 84 | - certificatesigningrequests 85 | verbs: 86 | - list 87 | - watch 88 | - apiGroups: 89 | - storage.k8s.io 90 | resources: 91 | - storageclasses 92 | - volumeattachments 93 | verbs: 94 | - list 95 | - watch 96 | - apiGroups: 97 | - admissionregistration.k8s.io 98 | resources: 99 | - mutatingwebhookconfigurations 100 | - validatingwebhookconfigurations 101 | verbs: 102 | - list 103 | - watch 104 | - apiGroups: 105 | - networking.k8s.io 106 | resources: 107 | - networkpolicies 108 | verbs: 109 | - list 110 | - watch 111 | -------------------------------------------------------------------------------- /manifests/ks-metrics-crbind.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kube-state-metrics 6 | app.kubernetes.io/version: v1.8.0 7 | name: kube-state-metrics 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: kube-state-metrics 12 | subjects: 13 | - kind: ServiceAccount 14 | name: kube-state-metrics 15 | namespace: kube-system 16 | -------------------------------------------------------------------------------- /manifests/ks-metrics-sa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kube-state-metrics 6 | app.kubernetes.io/version: v1.8.0 7 | name: kube-state-metrics 8 | namespace: kube-system 9 | -------------------------------------------------------------------------------- /manifests/ks-metrics-srv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kube-state-metrics 6 | app.kubernetes.io/version: v1.8.0 7 | name: kube-state-metrics 8 | namespace: kube-system 9 | spec: 10 | clusterIP: None 11 | ports: 12 | - name: http-metrics 13 | port: 8080 14 | targetPort: http-metrics 15 | - name: telemetry 16 | port: 8081 17 | targetPort: telemetry 18 | selector: 19 | app.kubernetes.io/name: kube-state-metrics 20 | -------------------------------------------------------------------------------- /manifests/ksmetrics-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kube-state-metrics 6 | app.kubernetes.io/version: v1.8.0 7 | name: kube-state-metrics 8 | namespace: kube-system 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app.kubernetes.io/name: kube-state-metrics 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/name: kube-state-metrics 18 | app.kubernetes.io/version: v1.8.0 19 | spec: 20 | containers: 21 | - image: quay.io/coreos/kube-state-metrics:v1.8.0 22 | livenessProbe: 23 | httpGet: 24 | path: /healthz 25 | port: 8080 26 | initialDelaySeconds: 5 27 | timeoutSeconds: 5 28 | name: kube-state-metrics 29 | ports: 30 | - containerPort: 8080 31 | name: http-metrics 32 | - containerPort: 8081 33 | name: telemetry 34 | readinessProbe: 35 | httpGet: 36 | path: / 37 | port: 8081 38 | initialDelaySeconds: 5 39 | timeoutSeconds: 5 40 | nodeSelector: 41 | kubernetes.io/os: linux 42 | serviceAccountName: kube-state-metrics 43 | -------------------------------------------------------------------------------- /manifests/node-exporter-dset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: exporter 6 | app.kubernetes.io/name: node-exporter 7 | name: node-exporter 8 | namespace: monitoring 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/component: exporter 13 | app.kubernetes.io/name: node-exporter 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/component: exporter 18 | app.kubernetes.io/name: node-exporter 19 | spec: 20 | containers: 21 | - args: 22 | - --path.sysfs=/host/sys 23 | - --path.rootfs=/host/root 24 | - --no-collector.wifi 25 | - --no-collector.hwmon 26 | - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/pods/.+)($|/) 27 | - --collector.netclass.ignored-devices=^(veth.*)$ 28 | name: node-exporter 29 | image: prom/node-exporter 30 | ports: 31 | - containerPort: 9100 32 | protocol: TCP 33 | resources: 34 | limits: 35 | cpu: 250m 36 | memory: 180Mi 37 | requests: 38 | cpu: 102m 39 | memory: 180Mi 40 | volumeMounts: 41 | - mountPath: /host/sys 42 | # mountPropagation: HostToContainer 43 | name: sys 44 | readOnly: true 45 | - mountPath: /host/root 46 | # mountPropagation: HostToContainer 47 | name: root 48 | readOnly: true 49 | volumes: 50 | - hostPath: 51 | path: /sys 52 | name: sys 53 | - hostPath: 54 | path: / 55 | name: root 56 | --- 57 | kind: Service 58 | apiVersion: v1 59 | metadata: 60 | name: node-exporter 61 | namespace: monitoring 62 | annotations: 63 | prometheus.io/scrape: 'true' 64 | prometheus.io/port: '9100' 65 | spec: 66 | selector: 67 | app.kubernetes.io/component: exporter 68 | app.kubernetes.io/name: node-exporter 69 | ports: 70 | - name: node-exporter 71 | protocol: TCP 72 | port: 9100 73 | targetPort: 9100 -------------------------------------------------------------------------------- /manifests/prometheus-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: prometheus-deployment 5 | namespace: monitoring 6 | labels: 7 | app: prometheus-server 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: prometheus-server 13 | template: 14 | metadata: 15 | labels: 16 | app: prometheus-server 17 | spec: 18 | containers: 19 | - name: prometheus 20 | image: prom/prometheus 21 | args: 22 | - "--config.file=/etc/prometheus/prometheus.yml" 23 | - "--storage.tsdb.path=/prometheus/" 24 | ports: 25 | - containerPort: 9090 26 | volumeMounts: 27 | - name: prometheus-config-volume 28 | mountPath: /etc/prometheus/ 29 | - name: prometheus-storage-volume 30 | mountPath: /prometheus/ 31 | volumes: 32 | - name: prometheus-config-volume 33 | configMap: 34 | defaultMode: 420 35 | name: prometheus-server-conf 36 | 37 | - name: prometheus-storage-volume 38 | emptyDir: {} 39 | --- 40 | apiVersion: v1 41 | kind: Service 42 | metadata: 43 | name: prometheus-service 44 | namespace: monitoring 45 | annotations: 46 | prometheus.io/scrape: 'true' 47 | prometheus.io/port: '9090' 48 | 49 | spec: 50 | selector: 51 | app: prometheus-server 52 | type: NodePort 53 | ports: 54 | - port: 8080 55 | targetPort: 9090 56 | nodePort: 30000 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kr8s", 3 | "productName": "Kr8s", 4 | "version": "1.0.0", 5 | "main": "main.js", 6 | "description": "Kubernetes Developer Tool for Visualizing Metrics", 7 | "license": "MIT", 8 | "private": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/alexdevero/electron-react-webpack-boilerplate.git" 12 | }, 13 | "homepage": "https://github.com/oslabs-beta/kr8s#readme", 14 | "bugs": { 15 | "url": "https://github.com/oslabs-beta/kr8s/issues" 16 | }, 17 | "author": { 18 | "name": "Justin Stoddard, Reland Boyle, Duke Lee, and Adam Sheff", 19 | "email": "adamisheff@gmail.com", 20 | "url": "https://github.com/oslabs-beta/kr8s" 21 | }, 22 | "keywords": [ 23 | "kubernetes", 24 | "prometheus", 25 | "electron", 26 | "grafana", 27 | "open-source", 28 | "react", 29 | "webpack" 30 | ], 31 | "engines": { 32 | "node": ">=9.0.0", 33 | "npm": ">=5.0.0", 34 | "yarn": ">=1.0.0" 35 | }, 36 | "browserslist": [ 37 | "last 4 versions" 38 | ], 39 | "scripts": { 40 | "start": "cross-env NODE_ENV=production webpack --mode production --config=./webpack.build.config.js && electron --noDevServer .", 41 | "dev": "cross-env NODE_ENV=development webpack serve --hot --host 0.0.0.0 --config=./webpack.dev.config.js --mode development", 42 | "build": "cross-env NODE_ENV=production webpack --config webpack.build.config.js --mode production", 43 | "test": "jest" 44 | }, 45 | "jest": { 46 | "moduleNameMapper": { 47 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 48 | "\\.(css|less)$": "identity-obj-proxy" 49 | }, 50 | "setupFilesAfterEnv": [ 51 | "src/setupTests.js" 52 | ], 53 | "testEnvironment": "jsdom" 54 | }, 55 | "dependencies": { 56 | "@emotion/react": "^11.5.0", 57 | "@emotion/styled": "^11.3.0", 58 | "@material-ui/core": "^4.12.3", 59 | "@material-ui/styles": "^4.11.4", 60 | "@mui/icons-material": "^5.0.4", 61 | "@mui/material": "^5.0.4", 62 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5", 63 | "cors": "^2.8.5", 64 | "node-fetch": "^3.0.0", 65 | "postcss": "^8.3.6", 66 | "react": "^17.0.2", 67 | "react-dom": "^17.0.2", 68 | "react-router-dom": "^5.3.0" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.15.0", 72 | "@babel/plugin-transform-runtime": "^7.15.8", 73 | "@babel/preset-env": "^7.15.8", 74 | "@babel/preset-react": "^7.14.5", 75 | "@babel/runtime": "^7.15.4", 76 | "babel-jest": "^27.3.1", 77 | "babel-loader": "^8.2.2", 78 | "babel-polyfill": "^6.26.0", 79 | "cross-env": "^7.0.3", 80 | "css-loader": "^6.2.0", 81 | "electron": "^13.1.9", 82 | "electron-devtools-installer": "^3.2.0", 83 | "enzyme": "^3.11.0", 84 | "enzyme-adapter-react-16": "^1.15.6", 85 | "eslint": "^8.0.1", 86 | "file-loader": "^6.2.0", 87 | "html-webpack-plugin": "^5.3.2", 88 | "identity-obj-proxy": "^3.0.0", 89 | "jest": "^27.3.1", 90 | "jsdom": "18.0.1", 91 | "jsdom-global": "3.0.2", 92 | "mini-css-extract-plugin": "^2.2.0", 93 | "postcss-import": "^14.0.2", 94 | "postcss-loader": "^6.1.1", 95 | "postcss-nested": "^5.0.6", 96 | "postcss-preset-env": "^6.7.0", 97 | "postcss-pxtorem": "^6.0.0", 98 | "react-test-renderer": "^17.0.2", 99 | "style-loader": "^3.2.1", 100 | "supertest": "^6.1.6", 101 | "webpack": "^5.49.0", 102 | "webpack-cli": "^4.7.2", 103 | "webpack-dev-server": "^3.11.2" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | 'postcss-nested': {}, 5 | 'postcss-preset-env': {}, 6 | 'postcss-pxtorem': { 7 | rootValue: 16, 8 | unitPrecision: 5, 9 | propList: ['*'], 10 | selectorBlackList: ['html', 'body'], 11 | replace: true, 12 | mediaQuery: false, 13 | minPixelValue: 0 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /provisioning/dashboards/cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [], 3 | "__requires": [], 4 | "annotations": { 5 | "list": [] 6 | }, 7 | "editable": false, 8 | "gnetId": null, 9 | "graphTooltip": 0, 10 | "hideControls": false, 11 | "id": null, 12 | "links": [], 13 | "panels": [ 14 | { 15 | "aliasColors": {}, 16 | "bars": false, 17 | "dashLength": 10, 18 | "dashes": false, 19 | "datasource": "TestData DB", 20 | "fill": 1, 21 | "gridPos": { 22 | "h": 8, 23 | "w": 24, 24 | "x": 0, 25 | "y": 0 26 | }, 27 | "id": 2, 28 | "legend": { 29 | "alignAsTable": false, 30 | "avg": false, 31 | "current": false, 32 | "max": false, 33 | "min": false, 34 | "rightSide": false, 35 | "show": true, 36 | "total": false, 37 | "values": false 38 | }, 39 | "lines": true, 40 | "linewidth": 1, 41 | "links": [], 42 | "nullPointMode": "null", 43 | "percentage": false, 44 | "pointradius": 5, 45 | "points": false, 46 | "renderer": "flot", 47 | "repeat": null, 48 | "seriesOverrides": [], 49 | "spaceLength": 10, 50 | "stack": false, 51 | "steppedLine": false, 52 | "targets": [], 53 | "thresholds": [], 54 | "timeFrom": null, 55 | "timeShift": null, 56 | "title": "CPU Usage", 57 | "tooltip": { 58 | "shared": true, 59 | "sort": 0, 60 | "value_type": "individual" 61 | }, 62 | "type": "graph", 63 | "xaxis": { 64 | "buckets": null, 65 | "mode": "time", 66 | "name": null, 67 | "show": true, 68 | "values": [] 69 | }, 70 | "yaxes": [ 71 | { 72 | "format": "short", 73 | "label": null, 74 | "logBase": 1, 75 | "max": null, 76 | "min": null, 77 | "show": true 78 | }, 79 | { 80 | "format": "short", 81 | "label": null, 82 | "logBase": 1, 83 | "max": null, 84 | "min": null, 85 | "show": true 86 | } 87 | ] 88 | } 89 | ], 90 | "refresh": "", 91 | "rows": [], 92 | "schemaVersion": 16, 93 | "style": "dark", 94 | "tags": ["kubernetes"], 95 | "templating": { 96 | "list": [] 97 | }, 98 | "time": { 99 | "from": "now-6h", 100 | "to": "now" 101 | }, 102 | "timepicker": { 103 | "refresh_intervals": [ 104 | "5s", 105 | "10s", 106 | "30s", 107 | "1m", 108 | "5m", 109 | "15m", 110 | "30m", 111 | "1h", 112 | "2h", 113 | "1d" 114 | ], 115 | "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] 116 | }, 117 | "timezone": "browser", 118 | "title": "Cluster", 119 | "version": 0 120 | } 121 | -------------------------------------------------------------------------------- /provisioning/dashboards/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: Default # A uniquely identifiable name for the provider 5 | folder: Services # The folder where to place the dashboards 6 | type: file 7 | options: 8 | path: ./cluster.json 9 | # Default path for Windows: C:/Program Files/GrafanaLabs/grafana/public/dashboards 10 | # Default path for Linux is: /var/lib/grafana/dashboards -------------------------------------------------------------------------------- /src/APIcalls.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const process = require("process"); 5 | 6 | const apiCalls = {}; 7 | 8 | /* 9 | Retrieve data from Prometheus instance for running pods 10 | */ 11 | apiCalls.fetchPods = async () => { 12 | try { 13 | let response = await fetch("http://localhost:31000/api/podList"); 14 | response = await response.json(); 15 | return response; 16 | } catch { 17 | console.log("Error occured fetching pods"); 18 | } 19 | }; 20 | 21 | /* 22 | Retrieve data from Prometheus instance for running nodes 23 | */ 24 | apiCalls.fetchNodes = async () => { 25 | try { 26 | let response = await fetch("http://localhost:31000/api/nodeList"); 27 | response = await response.json(); 28 | return response; 29 | } catch { 30 | console.log("Error occured fetching nodes"); 31 | } 32 | }; 33 | 34 | /* 35 | Generate a valid API key for the user 36 | 37 | The API Key will be required to make subsequent 38 | requests to grafana for the user 39 | */ 40 | apiCalls.createAPIkey = async () => { 41 | try { 42 | let respObj; 43 | let response = await fetch("http://localhost:32000/api/auth/keys", { 44 | method: "POST", 45 | mode: "no-cors", 46 | headers: { 47 | Accept: "*/*", 48 | "Content-Type": "application/json", 49 | }, 50 | body: JSON.stringify({ 51 | name: "newuser", 52 | role: "Admin", 53 | secondsToLive: 86400, 54 | }), 55 | }) 56 | .then((res) => res.json()) 57 | .then((data) => { 58 | respObj = data; 59 | }); 60 | return respObj.key; 61 | } catch { 62 | console.log("Error occured creating API key"); 63 | } 64 | }; 65 | 66 | /* 67 | This imports the Kr8s custom dashboard into the user's Grafana 68 | 69 | The dashboard needs to be imported to grafana for the iframes 70 | to be available to the user 71 | 72 | A valid APIKey is required Grafana to accept the request 73 | */ 74 | const grafanaDashboard = fs.readFileSync( 75 | path.join(process.cwd(), "/grafana-kr8s-dashboard.json") 76 | ); 77 | apiCalls.grafanaDashboardPostRequest = async (APIKey) => { 78 | try { 79 | let response = await fetch("http://localhost:32000/api/dashboards/db", { 80 | method: "POST", 81 | mode: "no-cors", 82 | headers: { 83 | Accept: "*/*", 84 | "Content-Type": "application/json", 85 | Authorization: `Bearer ${APIKey}`, 86 | }, 87 | body: grafanaDashboard, 88 | }); 89 | } catch { 90 | console.log("Error occured posting dashboard to grafana"); 91 | } 92 | }; 93 | 94 | export default apiCalls; 95 | -------------------------------------------------------------------------------- /src/__tests__/Banner.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Banner from "../components/Banner.jsx"; 4 | 5 | import { mount, shallow } from "enzyme"; 6 | 7 | describe('Test Banner Component', () => { 8 | const testItems = [ 9 | {header: 'Header1', value: 1}, 10 | {header: 'Header2', value: 2}, 11 | {header: 'Header3', value: 3} 12 | ]; 13 | 14 | it('Component accepts items props', ()=>{ 15 | const wrapper = mount() 16 | 17 | expect(wrapper.props().items).toEqual(testItems); 18 | wrapper.setProps({items: [{header: 'test1', value: 10}]}); 19 | expect(wrapper.props().items[0].header).toEqual('test1'); 20 | }); 21 | 22 | 23 | it('Component has all props passed down', ()=>{ 24 | const wrapper = mount() 25 | expect(wrapper.props().items).toHaveLength(3); 26 | }); 27 | 28 | }) -------------------------------------------------------------------------------- /src/__tests__/Dashboard.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | 4 | import Dashboard from "../containers/Dashboard.jsx"; 5 | import Banner from "../components/Banner.jsx"; 6 | import { ExpansionPanelActions } from "@material-ui/core"; 7 | 8 | describe('Test for Dashboard Component Rendering The Proper Information', () => { 9 | let wrapper; 10 | 11 | beforeAll(() => { 12 | wrapper = mount( 13 | ); 18 | }); 19 | 20 | 21 | test('Should have the correct prop values', () => { 22 | expect(wrapper.props().numNodes).toBe(2); 23 | expect(wrapper.props().numPods).toBe(5); 24 | expect(wrapper.props().numContainers).toBe(24); 25 | }); 26 | 27 | test('Should pass prop values down to the Banner Component', () => { 28 | expect(wrapper.find("Banner").props().items[0].value).toBe(2); 29 | expect(wrapper.find("Banner").props().items[1].value).toBe(5); 30 | expect(wrapper.find("Banner").props().items[2].value).toBe(24); 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/__tests__/List.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import List from "../components/List.jsx"; 4 | import TableRow from "@mui/material/TableRow"; 5 | import TableCell from "@mui/material/TableCell"; 6 | 7 | import { shallow, mount } from "enzyme"; 8 | 9 | describe("test to see if List component is rendering and functioning as expected", () => { 10 | let wrapper; 11 | const testHeaders = [ 12 | { id: "name", label: "Name", minWidth: 100, align: "center" }, 13 | { id: "header1", label: "Header1", minWidth: 100, align: "center" }, 14 | { id: "header2", label: "Header2", minWidth: 100, align: "center" }, 15 | { id: "header3", label: "Header3", minWidth: 100, align: "center" }, 16 | { id: "header4", label: "Header4", minWidth: 100, align: "center" }, 17 | ]; 18 | const testValues = [ 19 | { 20 | name: "test1", 21 | header1: true, 22 | header2: 4, 23 | header3: "yes", 24 | header4: "good", 25 | }, 26 | { 27 | name: "test2", 28 | header1: false, 29 | header2: 7, 30 | header3: "no", 31 | header4: "good", 32 | }, 33 | { 34 | name: "test3", 35 | header1: true, 36 | header2: 2, 37 | header3: "yes", 38 | header4: "bad", 39 | }, 40 | { 41 | name: "test4", 42 | header1: true, 43 | header2: 1, 44 | header3: "yes", 45 | header4: "average", 46 | }, 47 | { 48 | name: "test5", 49 | header1: true, 50 | header2: 11, 51 | header3: "yes", 52 | header4: "average", 53 | }, 54 | { 55 | name: "test6", 56 | header1: false, 57 | header2: 9, 58 | header3: "no", 59 | header4: "good", 60 | }, 61 | ]; 62 | beforeAll(() => { 63 | wrapper = mount( 64 | 69 | ); 70 | }); 71 | beforeEach(() => {}); 72 | test("number of rows should be according to the number of listValues prop plus the header - 7 in test", () => { 73 | expect(wrapper.find(TableRow)).toHaveLength(7); 74 | }); 75 | test("number of columns should be according to the listValueHeaders prop - 5 in test", () => { 76 | expect(wrapper.find(TableRow).at(0).find(TableCell)).toHaveLength(5); 77 | }); 78 | test("number of cells should be according to the listValueHeaders and listValues props - 35 in test", () => { 79 | expect(wrapper.find(TableCell)).toHaveLength(35); 80 | }); 81 | test("first column header should be 'Name'", () => { 82 | expect(wrapper.find(TableRow).at(0).props().children[0].key).toBe("name"); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/__tests__/Nodes.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Nodes from "../containers/Nodes.jsx"; 4 | 5 | import { shallow, mount } from "enzyme"; 6 | 7 | describe("test to see if Nodes page is rendering and functioning as expected", () => { 8 | let wrapper; 9 | const nodes = [ 10 | { 11 | metadata: { 12 | name: "testNode", 13 | }, 14 | status: { 15 | conditions: [ 16 | { status: false }, 17 | { status: false }, 18 | { status: false }, 19 | { status: true }, 20 | ], 21 | }, 22 | }, 23 | ]; 24 | beforeAll(() => { 25 | wrapper = mount(); 26 | }); 27 | beforeEach(() => { 28 | const nodeValues = { 29 | node: null, 30 | memorypressure: null, 31 | diskpressure: null, 32 | pidpressure: null, 33 | ready: null, 34 | }; 35 | }); 36 | test("number of nodes count incrementing functionality", () => { 37 | expect(wrapper.find("Banner").props().items[0].value).toBe(1); 38 | }); 39 | test("render 1 Banner component in Nodes page", () => { 40 | expect(wrapper.find("Banner")).toHaveLength(1); 41 | }); 42 | test("render 3 iframes in Nodes page", () => { 43 | expect(wrapper.find("iframe")).toHaveLength(3); 44 | }); 45 | test("render 1 List component in Nodes page", () => { 46 | expect(wrapper.find("List")).toHaveLength(1); 47 | }); 48 | test("5 list headers passed to List component", () => { 49 | expect(wrapper.find("List").props().listValueHeaders).toHaveLength(5); 50 | }); 51 | test("list reroute prop is falsy", () => { 52 | expect(wrapper.find("List").props().reroute).toBeFalsy(); 53 | }); 54 | test("list listValue prop object", () => { 55 | expect( 56 | wrapper.find("List").props().listValue[0].memorypressure 57 | ).toBeFalsy(); 58 | expect(wrapper.find("List").props().listValue[0].diskpressure).toBeFalsy(); 59 | expect(wrapper.find("List").props().listValue[0].pidpressure).toBeFalsy(); 60 | expect(wrapper.find("List").props().listValue[0].ready).toBeTruthy(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/__tests__/PodView.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import PodView from "../components/PodView.jsx"; 4 | 5 | import { shallow, mount } from "enzyme"; 6 | 7 | describe("test to see if PodView page is rendering and functioning as expected", () => { 8 | let wrapper; 9 | const myPod = { 10 | state: { 11 | info: { 12 | pod: "testPod", 13 | initialized: "true", 14 | ready: "true", 15 | containersReady: "true", 16 | podScheduled: "true", 17 | numContainers: 1, 18 | containers: [ 19 | { 20 | name: "testContainer", 21 | ready: "true", 22 | restartCount: 0, 23 | state: { running: "true" }, 24 | }, 25 | ], 26 | }, 27 | }, 28 | }; 29 | beforeAll(() => { 30 | wrapper = mount(); 31 | }); 32 | beforeEach(() => {}); 33 | test("pod view container count", () => { 34 | expect(wrapper.find("Banner").props().items[0].value).toBe(1); 35 | }); 36 | test("render 1 Banner component in Pod view page", () => { 37 | expect(wrapper.find("Banner")).toHaveLength(1); 38 | }); 39 | test("render 0 iframes in Pod view page", () => { 40 | expect(wrapper.find("iframe")).toHaveLength(0); 41 | }); 42 | test("render 1 List component in Pod view page", () => { 43 | expect(wrapper.find("List")).toHaveLength(1); 44 | }); 45 | test("3 list headers passed to List component", () => { 46 | expect(wrapper.find("List").props().listValueHeaders).toHaveLength(3); 47 | }); 48 | test("list reroute prop is falsy", () => { 49 | expect(wrapper.find("List").props().reroute).toBeFalsy(); 50 | }); 51 | test("list listValue prop object", () => { 52 | expect(wrapper.find("List").props().listValue[0].name).toBe( 53 | "testContainer" 54 | ); 55 | expect(wrapper.find("List").props().listValue[0].ready).toBeTruthy(); 56 | expect(wrapper.find("List").props().listValue[0].restarts).toBe(0); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/Pods.test.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Pods from "../containers/Pods.jsx"; 4 | 5 | import { shallow, mount } from "enzyme"; 6 | 7 | describe("test to see if Pods page is rendering and functioning as expected", () => { 8 | let wrapper, runningPods, pendingPods, failedPods, unknownPods, succeededPods; 9 | const pods = [ 10 | { 11 | status: { 12 | phase: "Running", 13 | conditions: [ 14 | { status: true }, 15 | { status: true }, 16 | { status: true }, 17 | { status: true }, 18 | ], 19 | containerStatuses: true, 20 | }, 21 | metadata: { name: "testPod" }, 22 | spec: { containers: [1, 2, 3] }, 23 | }, 24 | ]; 25 | beforeAll(() => { 26 | wrapper = mount(); 27 | }); 28 | beforeEach(() => { 29 | (runningPods = 0), 30 | (pendingPods = 0), 31 | (failedPods = 0), 32 | (unknownPods = 0), 33 | (succeededPods = 0); 34 | }); 35 | test("pod status incrementing functionality", () => { 36 | expect(wrapper.find("Banner").props().items[0].value).toBe(1); 37 | expect(wrapper.find("Banner").props().items[1].value).toBe(0); 38 | expect(wrapper.find("Banner").props().items[2].value).toBe(0); 39 | expect(wrapper.find("Banner").props().items[3].value).toBe(0); 40 | expect(wrapper.find("Banner").props().items[4].value).toBe(0); 41 | }); 42 | test("render 1 Banner component in Pods page", () => { 43 | expect(wrapper.find("Banner")).toHaveLength(1); 44 | }); 45 | test("render 5 iframes in Pods page", () => { 46 | expect(wrapper.find("iframe")).toHaveLength(5); 47 | }); 48 | test("render 1 List component in Pods page", () => { 49 | expect(wrapper.find("List")).toHaveLength(1); 50 | }); 51 | test("6 list headers passed to List component", () => { 52 | expect(wrapper.find("List").props().listValueHeaders).toHaveLength(6); 53 | }); 54 | test("list reroute prop is truthy", () => { 55 | expect(wrapper.find("List").props().reroute).toBeTruthy(); 56 | }); 57 | test("list listValue prop object", () => { 58 | expect(wrapper.find("List").props().listValue[0].initialized).toBeTruthy(); 59 | expect(wrapper.find("List").props().listValue[0].ready).toBeTruthy(); 60 | expect( 61 | wrapper.find("List").props().listValue[0].containersReady 62 | ).toBeTruthy(); 63 | expect(wrapper.find("List").props().listValue[0].podScheduled).toBeTruthy(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const express = require("express"); 3 | 4 | const app = express(); 5 | 6 | const clientNodeAPI = "http://localhost:31000"; 7 | 8 | describe("client-node API route integration", () => { 9 | describe("GET /api/podList", () => { 10 | test("responds with application/json content type and 200 status", () => 11 | request(clientNodeAPI) 12 | .get("/api/podList") 13 | .expect("Content-Type", "application/json; charset=utf-8") 14 | .expect(200)); 15 | }); 16 | describe("GET /api/nodeList", () => { 17 | test("responds with application/json content type and 200 status", () => 18 | request(clientNodeAPI) 19 | .get("/api/nodeList") 20 | .expect("Content-Type", "application/json; charset=utf-8") 21 | .expect(200)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/assets/css/App.module.css: -------------------------------------------------------------------------------- 1 | /* Main CSS file */ 2 | 3 | @import "_example/_example.css"; 4 | 5 | body::-webkit-scrollbar { 6 | display: none; 7 | } 8 | 9 | .AppContainer { 10 | height: 100vh; 11 | width: 100vw; 12 | display: flex; 13 | flex-direction: row; 14 | } 15 | 16 | #header { 17 | height: 43px; 18 | 19 | color: #F1F0EF; 20 | font-style: normal; 21 | font-weight: 600; 22 | font-size: 36px; 23 | line-height: 43px; 24 | letter-spacing: 0.216667px; 25 | 26 | margin: 40px 0px; 27 | } 28 | 29 | .routerWrapper { 30 | margin-left: 35px; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/assets/css/Banner.module.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | background: #1C1C21; 3 | height: 124px; 4 | width: 1130px; 5 | left: 295px; 6 | top: 224px; 7 | border-radius: 4px; 8 | border: 1px solid #979698; 9 | 10 | display: flex; 11 | justify-content: space-around; 12 | } 13 | 14 | .items { 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | width: 100%; 19 | border-right: 1px solid #9796982c; 20 | } 21 | 22 | .items:last-child { 23 | border: none 24 | } 25 | 26 | .itemHeading { 27 | border-radius: nullpx; 28 | font-family: Lato; 29 | font-size: 14px; 30 | font-style: normal; 31 | font-weight: 600; 32 | line-height: 17px; 33 | letter-spacing: 0.21666666865348816px; 34 | text-align: center; 35 | line-height: 5px; 36 | padding-top: 10px; 37 | color: #979698; 38 | } 39 | 40 | .itemValue { 41 | border-radius: nullpx; 42 | font-family: Lato; 43 | font-size: 36px; 44 | font-style: normal; 45 | font-weight: 500; 46 | line-height: 43px; 47 | letter-spacing: 0.21666666865348816px; 48 | text-align: center; 49 | line-height: 15px; 50 | color: #F1F0EF; 51 | } -------------------------------------------------------------------------------- /src/assets/css/ClusterConnect.module.css: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | position: absolute; 3 | width: 440px; 4 | height: 488px; 5 | margin-left: 480px; 6 | top: 175px; 7 | 8 | background: #161519; 9 | border: 1px solid #FFFFFF; 10 | box-sizing: border-box; 11 | border-radius: 4px; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | } 16 | 17 | #clusters { 18 | padding-bottom: 10px 19 | } 20 | 21 | .clusterContainer { 22 | height: 45px; 23 | width: 367px; 24 | 25 | background: #2f6eb6c5; 26 | backdrop-filter: blur(4px); 27 | border-radius: 4px; 28 | border: 1px solid #FFFFFF; 29 | 30 | /* Style and Center Text */ 31 | color: #FFFFFF; 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | } 36 | 37 | .clusterLink { 38 | text-decoration: none; 39 | } 40 | 41 | #logo { 42 | margin-top: 50px; 43 | margin-bottom: 75px; 44 | } 45 | 46 | #newContainer { 47 | height: 45px; 48 | width: 367px; 49 | 50 | border: 1px solid #888888; 51 | border-radius: 4px; 52 | 53 | /* Note: backdrop-filter has minimal browser support */ 54 | background: #000000b7; 55 | backdrop-filter: blur(4px); 56 | 57 | /* Style and Center Text */ 58 | color: #888888; 59 | display: flex; 60 | align-items: center; 61 | justify-content: center; 62 | } 63 | 64 | #newContainer:hover { 65 | cursor: not-allowed; 66 | } -------------------------------------------------------------------------------- /src/assets/css/Dashboard.module.css: -------------------------------------------------------------------------------- 1 | .DashboardContainer { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .linegraph { 7 | width: 550px; 8 | height: 200px; 9 | margin-top: 10px; 10 | border: 1px solid #979698; 11 | border-radius: 4px; 12 | } 13 | 14 | .grafanaColumn { 15 | width: 550px; 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: space-between; 19 | } 20 | 21 | #grafanaDisplays { 22 | width: 1130px; 23 | 24 | display: flex; 25 | justify-content: space-between; 26 | } 27 | 28 | #speedometers { 29 | width: 1130px; 30 | margin-top: 10px; 31 | display: flex; 32 | flex-direction: row; 33 | justify-content: space-between; 34 | } 35 | 36 | .speedo { 37 | height: 160px; 38 | width: 265px; 39 | border: 1px solid #979698; 40 | border-radius: 4px; 41 | } 42 | 43 | .speedoRow { 44 | display: flex; 45 | justify-content: space-between; 46 | margin-top: 10px; 47 | } -------------------------------------------------------------------------------- /src/assets/css/Header.module.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | height: fit-content; 3 | width: fit-content; 4 | } 5 | 6 | .headerContent { 7 | font-size: 5.5em; 8 | font-style: italic; 9 | font-weight: 400; 10 | color: rgb(255, 94, 0); 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/css/LineGraph.module.css: -------------------------------------------------------------------------------- 1 | 2 | .lineGraphContainer { 3 | border: solid #31A72B 2px; 4 | margin: 20px; 5 | height: fit-content; 6 | width: fit-content; 7 | border-radius: .5em; 8 | } -------------------------------------------------------------------------------- /src/assets/css/List.module.css: -------------------------------------------------------------------------------- 1 | .rows { 2 | color: white; 3 | text-decoration: none; 4 | } 5 | 6 | .header { 7 | color: #979698 !important; 8 | color: "#F1F0EF"; 9 | font-weight: "normal"; 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/css/NodeView.module.css: -------------------------------------------------------------------------------- 1 | .containersContainer { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: center; 6 | } 7 | 8 | .containersContainerHeader { 9 | width: 100%; 10 | height: 400px; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: top; 14 | justify-content: space-evenly; 15 | } 16 | 17 | .lineGraph { 18 | width: 40%; 19 | height: 45%; 20 | margin-right: 25px; 21 | border: 1px solid #979698; 22 | box-sizing: border-box; 23 | border-radius: 4px; 24 | } 25 | 26 | .lineGraph:last-child { 27 | margin-right: 0; 28 | } 29 | 30 | .nodeNumbers { 31 | width: 100%; 32 | 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: space-evenly; 36 | 37 | background-color: #1C1C21; 38 | color: #979698a8; 39 | 40 | border-radius: 4px; 41 | border: 1px solid #979698; 42 | } -------------------------------------------------------------------------------- /src/assets/css/Nodes.module.css: -------------------------------------------------------------------------------- 1 | #nodesContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .lineGraph { 8 | width: 350px; 9 | height: 250px; 10 | 11 | border: 1px solid #979698; 12 | border-radius: 4px; 13 | } 14 | 15 | #lineGraphs { 16 | width: 1130px; 17 | margin: 20px 0px; 18 | 19 | display: flex; 20 | justify-content: space-between; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/assets/css/PodView.module.css: -------------------------------------------------------------------------------- 1 | .containersContainer { 2 | display: flex; 3 | flex-wrap: wrap; 4 | flex-direction: column; 5 | 6 | width: 100%; 7 | } 8 | 9 | .containersContainerHeader { 10 | width: 100%; 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | } 15 | 16 | .containersContainerList { 17 | margin-top: 50px; 18 | } -------------------------------------------------------------------------------- /src/assets/css/Pods.module.css: -------------------------------------------------------------------------------- 1 | .podsContainer { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .podsContainerHeader { 8 | width: 100%; 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | } 13 | 14 | .podsContainerMain { 15 | width: 1130px; 16 | /* height: 300px; */ 17 | margin-top: 30px; 18 | display: flex; 19 | } 20 | 21 | .podsiframe { 22 | border: 1px solid #979698; 23 | border-radius: 4px; 24 | margin-bottom: 20px; 25 | } 26 | 27 | .podsContainerList { 28 | width: 1130px; 29 | height: 30%; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .podsContainerList:first-child { 35 | margin: auto; 36 | } 37 | 38 | .podsContainerList h3 { 39 | color: #f1f0ef; 40 | } 41 | 42 | #columnLeft { 43 | width: 50%; 44 | display: flex; 45 | flex-direction: row; 46 | flex-wrap: wrap; 47 | justify-content: flex-start; 48 | } 49 | 50 | #columnRight { 51 | width: 50%; 52 | display: flex; 53 | flex-direction: row; 54 | flex-wrap: wrap; 55 | justify-content: flex-end; 56 | } 57 | 58 | #list { 59 | width: 1130px; 60 | } 61 | 62 | #firstPodiFrame { 63 | margin-right: 20px; 64 | } -------------------------------------------------------------------------------- /src/assets/css/Sidebar.module.css: -------------------------------------------------------------------------------- 1 | .pageNameContainer { 2 | color: rgb(247, 246, 248); 3 | margin: 5px 0; 4 | padding: 0 10px !important; 5 | } 6 | 7 | .clickedPage { 8 | background-color: rgba(241, 240, 239, 0.1); 9 | width: 100%; 10 | height: 100%; 11 | padding: 15px; 12 | border-radius: 4px; 13 | } 14 | 15 | .unClickedPage { 16 | color: rgba(241, 240, 239, 0.5); 17 | width: 100%; 18 | height: 100%; 19 | padding: 15px; 20 | border-radius: 4px; 21 | } 22 | 23 | .clickedPage a { 24 | color: #f1f0ef; 25 | text-decoration: none; 26 | margin: 5px 0 5px 10px; 27 | font-weight: 600; 28 | } 29 | 30 | .unClickedPage a { 31 | color: rgba(241, 240, 239, 0.5); 32 | text-decoration: none; 33 | margin: 5px 0 5px 10px; 34 | font-weight: 600; 35 | } 36 | 37 | .divider { 38 | background: rgba(128, 128, 128, 0.25); 39 | } 40 | 41 | .logo svg { 42 | margin-left: 10px; 43 | margin-bottom: 20px; 44 | } 45 | 46 | #box { 47 | background-color: rgb(235, 63, 143) !important; 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/css/Speedometer.module.css: -------------------------------------------------------------------------------- 1 | 2 | .speedometerContainer { 3 | border: solid #31A72B 2px; 4 | margin: 20px; 5 | height: fit-content; 6 | width: fit-content; 7 | border-radius: .5em; 8 | } -------------------------------------------------------------------------------- /src/assets/css/Tile.module.css: -------------------------------------------------------------------------------- 1 | .tileContainer { 2 | background-image: radial-gradient(circle, #5f5a5d, #4f4a4d, #3f3b3c, #2f2c2d, #272425, #1f1c1d, #171515, #131111, #0e0c0c, #070606, #000000); 3 | color: #31A72B; 4 | border: solid #31A72B 3px; 5 | border-radius: 50%; 6 | margin: 20px; 7 | height: 150px; 8 | width: 150px; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .tileHeader { 16 | text-align: center; 17 | color: #daa71d; 18 | } 19 | 20 | .tileValue { 21 | align-self: center; 22 | color: #26b81e; 23 | } -------------------------------------------------------------------------------- /src/assets/css/_example/_example.css: -------------------------------------------------------------------------------- 1 | /* Example stylesheet */ 2 | @media screen and (prefers-color-scheme: light), 3 | screen and (prefers-color-scheme: no-preference) { 4 | /* Light theme */ 5 | body { 6 | color: #000; 7 | background-color: rgba(22, 21, 25, 1); 8 | margin: 0; 9 | font-family: 'Lato', sans-serif; 10 | } 11 | } 12 | 13 | @media screen and (prefers-color-scheme: dark) { 14 | /* Dark theme */ 15 | body { 16 | color: #201d1e; 17 | background-color: rgba(22, 21, 25, 1); 18 | margin: 0; 19 | font-family: 'Lato', sans-serif; 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/assets/css/imgs/KR8S-Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/KR8S-Background.png -------------------------------------------------------------------------------- /src/assets/css/imgs/KR8S-PNG-1600-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/KR8S-PNG-1600-900.png -------------------------------------------------------------------------------- /src/assets/css/imgs/Kr8s2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/Kr8s2.jpg -------------------------------------------------------------------------------- /src/assets/css/imgs/Transparent_Image_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/Transparent_Image_3.png -------------------------------------------------------------------------------- /src/assets/css/imgs/icon-24@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/icon-24@256.png -------------------------------------------------------------------------------- /src/assets/css/imgs/kr8s-connect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/css/imgs/kr8s-text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/css/imgs/kr8s-wheel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/css/imgs/kr8s.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/kr8s.ico -------------------------------------------------------------------------------- /src/assets/css/imgs/preview_1_450x120.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/preview_1_450x120.jpg -------------------------------------------------------------------------------- /src/assets/css/imgs/preview_2_280x180.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/preview_2_280x180.jpg -------------------------------------------------------------------------------- /src/assets/css/imgs/under-construction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kr8s/eb90be191ef4d4c05dd432c8e5c746cb3cfc7f48/src/assets/css/imgs/under-construction.png -------------------------------------------------------------------------------- /src/components/Banner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from '../assets/css/Banner.module.css'; 3 | 4 | /* 5 | Banner will receive items to display in props as an array of objects 6 | Expected format: [{header: 'string', value: integer}] 7 | 8 | Banner will display dynamically based on the number of items received 9 | */ 10 | export default function Banner(props) { 11 | 12 | const items = props.items.map(item => { 13 | return ( 14 |
15 |

{item.header}

16 |

{item.value}

17 |
18 | ); 19 | }) 20 | 21 | return ( 22 |
23 | {items} 24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /src/components/ClusterConnect.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import apiCalls from "../APIcalls.js"; 4 | import styles from "../assets/css/ClusterConnect.module.css"; 5 | import logo from "../assets/css/imgs/kr8s-connect.svg"; 6 | 7 | 8 | export default function ClusterConnect(props) { 9 | 10 | /* 11 | Build links to user clusters 12 | 13 | Each link will create a Grafana api key for the user 14 | and import the Kr8s Grafana dashboard before then 15 | calling getClusterInfo, which will redirect the user to 16 | their cluster dashboard and begin scraping data from Prometheus 17 | */ 18 | let clusters = props.clusters.map((cluster) => { 19 | return ( 20 | { 25 | const APIkey = await apiCalls.createAPIkey(); 26 | await apiCalls.grafanaDashboardPostRequest(APIkey); 27 | props.getClusterInfo(); 28 | }} 29 | > 30 |

{cluster}

31 | 32 | ); 33 | }); 34 | 35 | return ( 36 |
37 |
38 | 39 |
{clusters}
40 |
Connect To New Cluster
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from '../assets/css/Header.module.css'; 3 | 4 | // Header component is not currently used by Kr8s 5 | 6 | export default function Header(props) { 7 | 8 | return ( 9 |
10 |

{props.headerContent}

11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /src/components/LineGraph.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import style from "../assets/css/LineGraph.module.css"; 3 | 4 | // LineGraph component is not currently used by Kr8s 5 | 6 | export default function LineGraph(props) { 7 | return ( 8 |
9 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/List.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | 4 | import Paper from "@mui/material/Paper"; 5 | import Table from "@mui/material/Table"; 6 | import TableBody from "@mui/material/TableBody"; 7 | import TableCell from "@mui/material/TableCell"; 8 | import TableContainer from "@mui/material/TableContainer"; 9 | import TableHead from "@mui/material/TableHead"; 10 | import TablePagination from "@mui/material/TablePagination"; 11 | import TableRow from "@mui/material/TableRow"; 12 | 13 | import styles from "../assets/css/List.module.css"; 14 | 15 | // DUMMY DATA THAT SHOULD FLOW DOWN FROM PROPS 16 | // const nodesHeaders = [ 17 | // { id: 'node', label: 'Node', minWidth: 100}, 18 | // { id: 'ready', label: 'Ready', minWidth: 100 }, 19 | // { id: 'memorypressure', label: 'MemoryPressure', minWidth: 100 }, 20 | // { id: 'diskpressure', label: 'DiskPressure', minWidth: 100 }, 21 | // { id: 'pidPressure', label: 'PID Pressure', minWidth: 100 } 22 | // ]; 23 | 24 | // const nodesValues = [{nodes: 'node1', ready: 'true', memorypressure: 'good', diskpressure: 'high'}, 25 | // {nodes: 'node2', ready: 'false', memorypressure: 'bad', diskpressure: 'high'}, 26 | // {nodes: 'node3', ready: 'true', memorypressure: 'good', diskpressure: 'high'}] 27 | 28 | // const podsHeaders = [ 29 | // { id: 'pods', label: 'Pods', minWidth: 100 } 30 | // { id: 'initialized', label: 'Initialized', minWidth: 100 }, 31 | // { id: 'ready', label: 'Ready', minWidth: 100 }, 32 | // { id: 'containersReady', label: 'Containers Ready', minWidth: 100 }, 33 | // { id: 'podScheduled', label: 'Pod Scheduled', minWidth: 100 }, 34 | // { id: 'numContainers', label: 'Number of Containers', minWidth: 100 }] 35 | 36 | // const podsValues = [ 37 | // {pods: 'pod1', initialized: 'true', ready: 'true', containersReady: 'true', podScheduled: 'true', numContainers: 3} 38 | // {pods: 'pod2', initialized: 'true', ready: 'true', containersReady: 'false', podScheduled: 'true', numContainers: 1} 39 | // {pods: 'pod3', initialized: 'true', ready: 'false', containersReady: 'true', podScheduled: 'true', numContainers: 6} 40 | // ] 41 | 42 | // const listValueHeaders = nodesHeaders 43 | // const listValue = nodesValues 44 | 45 | export default function List(props) { 46 | const [page, setPage] = useState(0); 47 | const [rowsPerPage, setRowsPerPage] = useState(10); 48 | const [rowName, setRowName] = useState(""); 49 | 50 | const handleChangePage = (event, newPage) => { 51 | setPage(newPage); 52 | }; 53 | 54 | const handleChangeRowsPerPage = (event) => { 55 | setRowsPerPage(+event.target.value); 56 | setPage(0); 57 | }; 58 | 59 | function handleClick(e) { 60 | e.preventDefault(); 61 | props.setCurrentTarget(e.target.textContent); 62 | setRowName(e.target.textContent); 63 | return; 64 | } 65 | 66 | if (rowName.length && props.reroute) { 67 | return ( 68 | 74 | ); 75 | } 76 | 77 | return ( 78 | 93 | 94 | 95 | 96 | 97 | {props.listValueHeaders.map((header) => ( 98 | 107 | {header.label.toUpperCase()} 108 | 109 | ))} 110 | 111 | 112 | 113 | {props.listValue 114 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 115 | .map((row) => { 116 | if (props.reroute) { 117 | return ( 118 | 124 | {props.listValueHeaders.map((header) => { 125 | const value = row[header.id]; 126 | return ( 127 | 132 | 137 | {header.format && typeof value === "number" 138 | ? header.format(value) 139 | : value} 140 | 141 | 142 | ); 143 | })} 144 | 145 | ); 146 | } else { 147 | return ( 148 | 154 | {props.listValueHeaders.map((header) => { 155 | const value = row[header.id]; 156 | return ( 157 | 162 | 163 | {header.format && typeof value === "number" 164 | ? header.format(value) 165 | : value} 166 | 167 | 168 | ); 169 | })} 170 | 171 | ); 172 | } 173 | })} 174 | 175 |
176 |
177 | 195 |
196 | ); 197 | } 198 | -------------------------------------------------------------------------------- /src/components/NodeView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "../assets/css/NodeView.module.css"; 3 | 4 | // NodeView is currently disabled by Kr8s 5 | 6 | export default function NodeView(props) { 7 | const { node, memoryPressure, diskPressure, pidPressure, ready } = 8 | props.location.state.info; 9 | 10 | return ( 11 |
12 |
13 |
14 | 18 |
19 | 20 |
21 | 25 |
26 | 27 |
28 | 32 |
33 |
34 | 35 |
36 |

READY

37 |

|

38 |

39 | Disk Pressure 40 |

41 |

|

42 |

43 | Memory Pressure 44 |

45 |

|

46 |

47 | PID Pressure 48 |

49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/PodView.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Tile from "../components/Tile.jsx"; 4 | import List from "../components/List.jsx"; 5 | import Banner from "../components/Banner.jsx"; 6 | 7 | import styles from "../assets/css/PodView.module.css"; 8 | 9 | const containersHeaders = [ 10 | { id: "name", label: "Name", minWidth: 100, align: "center" }, 11 | { id: "ready", label: "Ready", minWidth: 100, align: "center" }, 12 | { id: "restarts", label: "Number of Restarts", minWidth: 100, align: "center" }, 13 | ]; 14 | 15 | export default function PodView(props) { 16 | const { 17 | pod, 18 | initialized, 19 | ready, 20 | containersReady, 21 | podScheduled, 22 | numContainers, 23 | containers, 24 | } = props.location.state.info; 25 | const containersValues = []; 26 | let runningContainers = 0; 27 | let failedContainers = 0; 28 | 29 | for (let i = 0; i < containers.length; i++) { 30 | const container = {}; 31 | container["name"] = containers[i].name; 32 | container["ready"] = containers[i].ready.toString(); 33 | container["restarts"] = containers[i].restartCount; 34 | 35 | if(containers[i].state.running) runningContainers++; 36 | else failedContainers++; 37 | 38 | containersValues.push(container); 39 | } 40 | 41 | return ( 42 |
43 | 44 |
45 | 46 | 53 | 54 |
55 | 56 |
57 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; 3 | import { createHashHistory } from 'history'; 4 | import Box from "@mui/material/Box"; 5 | import Drawer from "@mui/material/Drawer"; 6 | import Toolbar from "@mui/material/Toolbar"; 7 | import List from "@mui/material/List"; 8 | import Divider from "@mui/material/Divider"; 9 | import ListItem from "@mui/material/ListItem"; 10 | import styles from "../assets/css/Sidebar.module.css"; 11 | 12 | const drawerWidth = 240; 13 | 14 | export default function Sidebar(props) { 15 | const [currentPath, setCurrentPath] = useState(window.location.pathname); 16 | 17 | return ( 18 |
19 | 20 | 36 | 37 | 38 | 39 | 46 | 47 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 93 | 94 | 95 | 96 | 102 | 103 | 104 | 108 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
127 | 135 | 139 | 143 | 144 | 145 | { 148 | setCurrentPath("/dash"); 149 | }} 150 | > 151 | Dashboard 152 | 153 |
154 |
155 | 156 | 157 | 158 | 159 |
166 | 174 | 178 | 179 | 180 | { 183 | setCurrentPath("/nodes"); 184 | }} 185 | > 186 | Nodes 187 | 188 |
189 |
190 | 191 | 192 | 193 | 194 |
201 | 209 | 210 | 211 | 212 | { 215 | setCurrentPath("/pods"); 216 | }} 217 | > 218 | Pods 219 | 220 |
221 |
222 | 223 | 224 |
225 |
226 |
227 |
228 | ); 229 | } 230 | -------------------------------------------------------------------------------- /src/components/Speedometer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import style from "../assets/css/Speedometer.module.css"; 3 | 4 | // Speedometer component is not currently used by Kr8s 5 | 6 | export default function Speedometer(props) { 7 | return ( 8 |
9 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Tile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from '../assets/css/Tile.module.css'; 3 | 4 | // Tile Component is not currently used by Kr8s 5 | 6 | export default function Tile(props) { 7 | 8 | 9 | return ( 10 |
11 |

{props.tileHeader}

12 |

{props.tileValue}

13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /src/containers/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 3 | 4 | // Import components 5 | import Sidebar from "../components/Sidebar.jsx"; 6 | import ClusterConnect from "../components/ClusterConnect.jsx"; 7 | import Dashboard from "./Dashboard.jsx"; 8 | import Nodes from "./Nodes.jsx"; 9 | import Pods from "./Pods.jsx"; 10 | import PodView from "../components/PodView.jsx"; 11 | import NodeView from "../components/NodeView.jsx"; 12 | import style from "../assets/css/App.module.css"; 13 | import apiCalls from "../APIcalls.js"; 14 | 15 | export default function App() { 16 | 17 | const [connected, useConnected] = useState(false); 18 | const [clusterName, useClusterName] = useState(""); 19 | const [pods, setPods] = useState([]); 20 | const [nodes, setNodes] = useState([]); 21 | const [numContainers, setNumContainers] = useState(0); 22 | const [scrapeInterval, setScrapeInterval] = useState(15000); 23 | 24 | /* 25 | Function passed to ClusterConnect 26 | Invoked when user selects Cluster for connection 27 | Begins scraping from Prometheus 28 | */ 29 | function getClusterInfo() { 30 | // Set connected to true to display the sidebar 31 | useConnected(true); 32 | 33 | useClusterName("Local Cluster"); 34 | 35 | // Begin interval to scrape data from Prometheus 36 | // Interval for scraping is determined by scrapeInterval Hook 37 | const scrape = () => { 38 | apiCalls.fetchNodes().then((data) => { 39 | setNodes(data.items); 40 | }); 41 | 42 | apiCalls.fetchPods().then((data) => { 43 | setPods(data.items); 44 | 45 | // Retrieve number of containers by iterating over pods 46 | let containerCount = 0; 47 | for(const pod of data.items) { 48 | containerCount += pod.spec.containers.length; 49 | } 50 | setNumContainers(containerCount); 51 | }); 52 | } 53 | scrape(); 54 | setInterval(scrape, scrapeInterval); 55 | } 56 | 57 | return ( 58 | // Conditionally style the app background 59 |
60 | 61 |
62 | {/* Display Sidebar only if we are connected to a cluster */} 63 | {connected && } 64 |
65 | 66 |
{clusterName}
67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 82 | 83 | 84 | ( 87 | 90 | )} 91 | /> 92 | 93 | 94 | 97 | 98 | 99 | ( 102 | 105 | )} 106 | /> 107 | 108 | 109 | 113 | 114 | 115 | 116 |
117 |
118 |
119 |
120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/containers/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Banner from "../components/Banner.jsx"; 3 | import style from "../assets/css/Dashboard.module.css"; 4 | 5 | export default function Dashboard(props) { 6 | return ( 7 |
8 | 15 | 16 |
17 | 18 |
19 | 20 |
21 | 26 | 27 | 32 |
33 | 34 | 35 | 40 | 41 | 46 |
47 | 48 |
49 |
50 | 51 |
52 | 57 | 62 |
63 | 64 | 69 | 70 | 75 | 76 |
77 |
78 | 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/containers/Nodes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import Banner from "../components/Banner.jsx"; 4 | import List from "../components/List.jsx"; 5 | 6 | import style from "../assets/css/Nodes.module.css"; 7 | 8 | export default function Nodes(props) { 9 | const [myNode, setMyNode] = useState({}); 10 | 11 | /* 12 | **** Currently rerouting to NodeView is disabled **** 13 | **** To enable update reroute prop value passed to List component to '/NodeView' **** 14 | This function is passed down to the list component 15 | 16 | It is invoked onClick and is used to set the specific pod to display for a user 17 | when they are rerouted to the NodeView component, which displays information on 18 | a single Pod 19 | */ 20 | function setCurrentNode(nodeName) { 21 | for (let i = 0; i < nodesValues.length; i++) { 22 | if (nodesValues[i].node === nodeName) { 23 | setMyNode(nodesValues[i]); 24 | return; 25 | } 26 | } 27 | } 28 | 29 | /* 30 | Parse through passed in Nodes data from props 31 | This will organize the information to be displayed in the components below 32 | */ 33 | const nodesValues = [], 34 | nodesHeaders = [ 35 | { id: "node", label: "Node", minWidth: 100, align: "center" }, 36 | { id: "ready", label: "Ready", minWidth: 100, align: "center" }, 37 | { id: "memorypressure", label: "Memory Pressure", minWidth: 100, align: "center" }, 38 | { id: "diskpressure", label: "Disk Pressure", minWidth: 100, align: "center" }, 39 | { id: "pidPressure", label: "PID Pressure", minWidth: 100, align: "center" }, 40 | ]; 41 | 42 | let numNodes = props.nodes.length, 43 | numAvailableNodes = 0; 44 | 45 | props.nodes.forEach((node) => { 46 | // Increment Ready count for nodes 47 | for (let i = 0; i < node.status.conditions.length; i++) { 48 | const { type, status } = node.status.conditions[i]; 49 | if (type === "Ready" && status === "True") ++numAvailableNodes; 50 | } 51 | 52 | // Build and push current nodeValues object and push to nodesValues array 53 | const nodeValues = {}; 54 | nodeValues["node"] = node.metadata.name; 55 | nodeValues["memorypressure"] = node.status.conditions[0].status; 56 | nodeValues["diskpressure"] = node.status.conditions[1].status; 57 | nodeValues["pidPressure"] = node.status.conditions[2].status; 58 | nodeValues["ready"] = node.status.conditions[3].status; 59 | 60 | nodesValues.push(nodeValues); 61 | }); 62 | 63 | 64 | return ( 65 |
66 | 72 | 73 |
74 | 79 | 84 | 89 |
90 | 91 | 98 | 99 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/containers/Pods.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import Banner from "../components/Banner.jsx"; 4 | import List from "../components/List.jsx"; 5 | 6 | import styles from "../assets/css/Pods.module.css"; 7 | 8 | export default function Pods(props) { 9 | const [myPod, setMyPod] = useState({}); 10 | 11 | /* 12 | This function is passed down to the list component 13 | 14 | It is invoked onClick and is used to set the specific pod to display for a user 15 | when they are rerouted to the PodView component, which displays information on 16 | a single Pod 17 | */ 18 | function setCurrentPod(podName) { 19 | for (let i = 0; i < podsValues.length; i++) { 20 | if (podsValues[i].pod === podName) { 21 | setMyPod(podsValues[i]); 22 | return; 23 | } 24 | } 25 | } 26 | 27 | 28 | 29 | /* 30 | Parse through passed in Pods data from props 31 | This will organize the information to be displayed in the components below 32 | */ 33 | let runningPods = 0, 34 | pendingPods = 0, 35 | failedPods = 0, 36 | unknownPods = 0, 37 | succeededPods = 0; 38 | 39 | const podsValues = []; 40 | 41 | const podsHeaders = [ 42 | { id: "pod", label: "Pod", minWidth: 100, align: "center" }, 43 | { id: "initialized", label: "Initialized", minWidth: 100, align: "center" }, 44 | { id: "ready", label: "Ready", minWidth: 100, align: "center" }, 45 | { id: "containersReady", label: "Containers Ready", minWidth: 100, align: "center" }, 46 | { id: "podScheduled", label: "Pod Scheduled", minWidth: 100, align: "center" }, 47 | { id: "numContainers", label: "Number of Containers", minWidth: 100, align: "center" }, 48 | ]; 49 | 50 | props.pods.forEach((pod) => { 51 | const podValues = {}; 52 | 53 | // Increment count for the current Pod Status 54 | switch (pod.status.phase) { 55 | case "Running": 56 | runningPods++; 57 | break; 58 | case "Pending": 59 | pendingPods++; 60 | break; 61 | case "Failed": 62 | failedPods++; 63 | break; 64 | case "Unknown": 65 | unknownPods++; 66 | break; 67 | case "Succeeded": 68 | succeededPods++; 69 | break; 70 | } 71 | 72 | // Build Current Pod Values object and add to the podsValues Array 73 | podValues["pod"] = pod.metadata.name; 74 | podValues["initialized"] = pod.status.conditions[0].status; 75 | podValues["ready"] = pod.status.conditions[1].status; 76 | podValues["containersReady"] = pod.status.conditions[2].status; 77 | podValues["podScheduled"] = pod.status.conditions[3].status; 78 | podValues["numContainers"] = pod.spec.containers.length; 79 | podValues["containers"] = pod.status.containerStatuses; 80 | 81 | podsValues.push(podValues); 82 | }); 83 | 84 | 85 | 86 | 87 | return ( 88 |
89 |
90 |
91 | 101 |
102 | 103 |
104 |
105 | 112 | 119 | 126 |
127 | 128 |
129 | 136 | 143 |
144 |
145 | 146 |
147 | {/*

Deployed Pods

*/} 148 | 155 |
156 | 157 |
158 |
159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Kr8s 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './containers/App.jsx'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | 6 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 7 | const defaultInclude = path.resolve(__dirname, 'src') 8 | 9 | module.exports = { 10 | entry: ['babel-polyfill', './src/index.js'], 11 | // entry: { 12 | // app: './src/index.js', 13 | // }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/, 18 | use: [ 19 | MiniCssExtractPlugin.loader, 20 | 'css-loader', 21 | 'postcss-loader' 22 | ], 23 | include: defaultInclude 24 | }, 25 | { 26 | test: /\.jsx?$/, 27 | use: [{ loader: 'babel-loader' }], 28 | include: defaultInclude 29 | }, 30 | { 31 | test: /\.(jpe?g|png|gif)$/, 32 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], 33 | include: defaultInclude 34 | }, 35 | { 36 | test: /\.(eot|svg|ttf|woff|woff2)$/, 37 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], 38 | include: defaultInclude 39 | } 40 | ] 41 | }, 42 | target: 'electron-renderer', 43 | plugins: [ 44 | new HtmlWebpackPlugin({ template: './src/index.html' }), 45 | new MiniCssExtractPlugin({ 46 | // Options similar to the same options in webpackOptions.output 47 | // both options are optional 48 | filename: 'bundle.css', 49 | chunkFilename: '[id].css' 50 | }), 51 | new webpack.DefinePlugin({ 52 | 'process.env.NODE_ENV': JSON.stringify('production') 53 | }), 54 | // new MinifyPlugin() 55 | ], 56 | stats: { 57 | colors: true, 58 | children: false, 59 | chunks: false, 60 | modules: false 61 | }, 62 | optimization: { 63 | minimize: true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const { spawn } = require('child_process') 5 | 6 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 7 | const defaultInclude = path.resolve(__dirname, 'src') 8 | 9 | module.exports = { 10 | entry: ['babel-polyfill', './src/index.js'], 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/, 15 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }], 16 | include: defaultInclude 17 | }, 18 | { 19 | test: /\.jsx?$/, 20 | use: [{ loader: 'babel-loader' }], 21 | include: defaultInclude 22 | }, 23 | { 24 | test: /\.(jpe?g|png|gif)$/, 25 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], 26 | include: defaultInclude 27 | }, 28 | { 29 | test: /\.(eot|svg|ttf|woff|woff2)$/, 30 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], 31 | include: defaultInclude 32 | } 33 | ] 34 | }, 35 | target: 'electron-renderer', 36 | plugins: [ 37 | new HtmlWebpackPlugin({ template: './src/index.html' }), 38 | new webpack.DefinePlugin({ 39 | 'process.env.NODE_ENV': JSON.stringify('development') 40 | }) 41 | ], 42 | devtool: 'cheap-source-map', 43 | devServer: { 44 | historyApiFallback: true, 45 | contentBase: path.resolve(__dirname, 'dist'), 46 | stats: { 47 | colors: true, 48 | chunks: false, 49 | children: false 50 | }, 51 | before() { 52 | spawn( 53 | 'electron', 54 | ['.'], 55 | { shell: true, env: process.env, stdio: 'inherit' } 56 | ) 57 | .on('close', code => process.exit(0)) 58 | .on('error', spawnError => console.error(spawnError)) 59 | } 60 | } 61 | } 62 | --------------------------------------------------------------------------------