├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── Dockerfile
├── README.md
├── client
├── App.jsx
├── LoadingIndicator.jsx
├── LoginPage.jsx
├── RefreshRoute.jsx
├── actions
│ └── actions.js
├── assets
│ ├── aws-logo.png
│ └── pelicanLogo.png
├── components
│ ├── Buttons
│ │ ├── AddButton.jsx
│ │ ├── Button.jsx
│ │ ├── CoolButton.jsx
│ │ ├── DeletePod.jsx
│ │ ├── DeploymentButton.jsx
│ │ ├── DeploymentModal.jsx
│ │ ├── SubmitButton.jsx
│ │ ├── SubtractButton.jsx
│ │ └── TrashButton.jsx
│ ├── Configurations
│ │ ├── DeploymentConfigurations.jsx
│ │ ├── ImagesForm.jsx
│ │ ├── NodeConfiguration.jsx
│ │ ├── PodConfiguration.jsx
│ │ ├── SelectorsForm.jsx
│ │ └── ServicesCongifuration.jsx
│ ├── NamespaceDropdown.jsx
│ ├── Navbar.jsx
│ ├── deployments
│ │ ├── DeploymentRow.jsx
│ │ └── DeploymentTable.jsx
│ ├── nodes
│ │ ├── NodeRow.jsx
│ │ └── NodeTable.jsx
│ ├── pods
│ │ ├── PodRow.jsx
│ │ └── PodTable.jsx
│ └── services
│ │ ├── ServiceRow.jsx
│ │ └── ServiceTable.jsx
├── constants
│ ├── actionTypes.js
│ ├── awsRegions.js
│ └── tableInfoTemplate.js
├── containers
│ ├── LoginContainer.jsx
│ └── MainContainer.jsx
├── index.js
├── reducers
│ ├── appStateReducer.js
│ ├── awsAuthReducer.js
│ ├── clusterData.js
│ └── index.js
├── store.js
├── stylesheets
│ └── styles.scss
└── utils
│ └── yamlSyntaxHighlight.js
├── index.html
├── k8s-client
└── config.js
├── package-lock.json
├── package.json
├── server
├── controllers
│ ├── DeploymentController.js
│ ├── NamespaceController.js
│ ├── NodeController.js
│ ├── PodController.js
│ └── ServiceController.js
├── kubernetes-config.js
├── routes
│ ├── apiRouter.js
│ └── resourceRoutes
│ │ ├── deploymentRouter.js
│ │ ├── namespaceRouter.js
│ │ ├── nodeRouter.js
│ │ ├── podRouter.js
│ │ └── serviceRouter.js
└── server.js
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'],
3 | env: {
4 | browser: true,
5 | commonjs: true,
6 | es6: true,
7 | jest: true,
8 | node: true,
9 | jquery: true,
10 | },
11 | rules: {
12 | 'no-plusplus': 'off',
13 | 'no-console': 'off',
14 | 'jsx-a11y/href-no-hash': ['off'],
15 | 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }],
16 | 'max-len': [
17 | 'warn',
18 | {
19 | code: 80,
20 | tabWidth: 2,
21 | comments: 80,
22 | ignoreComments: false,
23 | ignoreTrailingComments: true,
24 | ignoreUrls: true,
25 | ignoreStrings: true,
26 | ignoreTemplateLiterals: true,
27 | ignoreRegExpLiterals: true,
28 | },
29 | ],
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build/
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | singleQuote: true,
4 | trailingComma: 'es5',
5 | };
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/pelican/e9195bde72e97bbc917252f5a779e9e0ce583a94/Dockerfile
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pelican, the Kubernetes Deployment Dashboard
2 |
3 | Pelican is a Kubernetes rollout dashboard that you can run locally connecting to any cluster or as a docker image connecting to an existing EKS cluster. Pelican allows you to check the status of your deployment, edit configurations, scale deployments, and rollout new images to your deployments using different deployment strategies.
4 | 
5 | ## Getting started
6 | Pelican can either be run locally or within your Kubernetes cluster using our supplied docker image
7 | ### Running locally
8 | First, ensure that your kubectl tool is configured to point to your desired cluster with `kubectl config current-context`. Your desired cloud provider will have detailed instructions on how to configure your kubectl interface. Once you have confirmed that kubectl is configured, simply fork and clone this repo. cd into the Pelican directory and sequentially run the following commands:
9 | ```
10 | npm run build
11 | npm start
12 | ```
13 | Pelican will serve on localhost:8080
14 | ### Running as a docker image
15 | Download our docker image from [Docker Hub](https://hub.docker.com/repository/docker/pelicank8s/pelicanfork8s). When you run the image, provide the following environment variables:
16 | ```
17 | K8S_CLUSTER_HOST=*your cluster endpoint*
18 | K8S_AUTH_TOKEN=*your cluster name*
19 | AWS_ACCESS_KEY_ID=*your AWS access key*
20 | AWS_SECRET_ACCESS_KEY=*your AWS secret access key*
21 | ```
22 | An example command would be:
23 |
24 | `docker run --env K8S_CLUSTER_HOST=[INSERT YOUR CLUSTER URL] --env K8S_AUTH_TOKEN=[INSERT YOUR CLUSTER NAME] --env K8S_CLUSTER_VERSION=[INSERT YOUR CLUSTER VERSION] --env AWS_ACCESS_KEY_ID=[INSERT YOUR AWS ACCESS KEY] --env AWS_SECRET_ACCESS_KEY=[INSERT YOUR AWS SECRET ACCESS KEY] --publish 3001:3000 --detach --name pelican ~~our image~~`
25 |
26 | The container should now be correctly configured and you can begin using Pelican.
27 |
28 | ## Using Pelican
29 | Pelican opens to your running pods for all namespaces. You can use the namespace dropdown selector to zero in on a particular namespace.
30 | Each object (pods, nodes, deployments, and nodes) have different information displayed and also has a button linking to an individual view. This individual view lists the object's configuration as well as current status. Deployments offer you the ability to change the image in the deployment and select a rollout method: standard, blue-green, and canary. Once the health of the rollout has been confirmed, your new deployment will be live.
31 | 
32 |
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/extensions */
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 | import {
6 | createMuiTheme,
7 | makeStyles,
8 | ThemeProvider,
9 | } from '@material-ui/core/styles';
10 | import MainContainer from './containers/MainContainer.jsx';
11 | import LoginContainer from './containers/LoginContainer.jsx';
12 | import './stylesheets/styles.scss';
13 |
14 | const mapStateToProps = ({ awsAuth }) => ({
15 | accessKeyId: awsAuth.accessKeyId,
16 | });
17 | const darkTheme = createMuiTheme({
18 | palette: {
19 | type: 'dark',
20 | primary: {
21 | main: '#00a0a0',
22 | },
23 | secondary: {
24 | main: '#11cb5f',
25 | },
26 | },
27 | });
28 |
29 | function App(props) {
30 | const { accessKeyId } = props;
31 | return (
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | export default connect(mapStateToProps)(App);
43 |
--------------------------------------------------------------------------------
/client/LoadingIndicator.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import Loader from 'react-loader-spinner';
4 | import { usePromiseTracker } from 'react-promise-tracker';
5 |
6 | const mapStateToProps = ({ appState }) => ({
7 | firstLoad: appState.firstLoad,
8 | });
9 |
10 | const LoadingIndicator = (props) => {
11 | const { promiseInProgress } = usePromiseTracker();
12 | return (
13 | promiseInProgress &&
14 | (props.firstLoad ? (
15 |
22 | ) : (
23 |
24 | {' '}
25 |
32 |
33 | Checking for updates
34 |
35 |
42 |
43 | ))
44 | );
45 | };
46 | export default connect(mapStateToProps)(LoadingIndicator);
47 |
--------------------------------------------------------------------------------
/client/LoginPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { connect } from 'react-redux';
3 | import Avatar from '@material-ui/core/Avatar';
4 | import Button from '@material-ui/core/Button';
5 | import CssBaseline from '@material-ui/core/CssBaseline';
6 | import TextField from '@material-ui/core/TextField';
7 | import FormControlLabel from '@material-ui/core/FormControlLabel';
8 | import Checkbox from '@material-ui/core/Checkbox';
9 | import Link from '@material-ui/core/Link';
10 | import Paper from '@material-ui/core/Paper';
11 | import Box from '@material-ui/core/Box';
12 | import Grid from '@material-ui/core/Grid';
13 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
14 | import Typography from '@material-ui/core/Typography';
15 | import InputLabel from '@material-ui/core/InputLabel';
16 | import MenuItem from '@material-ui/core/MenuItem';
17 | import FormHelperText from '@material-ui/core/FormHelperText';
18 | import FormControl from '@material-ui/core/FormControl';
19 | import Select from '@material-ui/core/Select';
20 | import { makeStyles } from '@material-ui/core/styles';
21 | import awsRegions from './constants/awsRegions';
22 | import * as actions from './actions/actions';
23 |
24 | function Copyright() {
25 | return (
26 |
27 | {'Copyright © '}
28 |
29 | Pelican
30 | {' '}
31 | {new Date().getFullYear()}
32 | {'.'}
33 |
34 | );
35 | }
36 |
37 | const useStyles = makeStyles((theme) => ({
38 | root: {
39 | height: '100vh',
40 | },
41 | image: {
42 | backgroundImage: 'url(https://source.unsplash.com/random?nautical)',
43 | backgroundRepeat: 'no-repeat',
44 | backgroundColor:
45 | theme.palette.type === 'light'
46 | ? theme.palette.grey[50]
47 | : theme.palette.grey[900],
48 | backgroundSize: 'cover',
49 | backgroundPosition: 'center',
50 | },
51 | paper: {
52 | margin: theme.spacing(8, 4),
53 | display: 'flex',
54 | flexDirection: 'column',
55 | alignItems: 'center',
56 | },
57 | avatar: {
58 | margin: theme.spacing(1),
59 | backgroundColor: theme.palette.secondary.main,
60 | },
61 | form: {
62 | width: '100%',
63 | marginTop: theme.spacing(1),
64 | },
65 | submit: {
66 | margin: theme.spacing(3, 0, 2),
67 | },
68 | }));
69 |
70 | const mapDispatchToProps = (dispatch) => ({
71 | setCredentials: (credentials) =>
72 | dispatch(actions.setCredentials(credentials)),
73 | });
74 |
75 | function SignInSide() {
76 | const classes = useStyles();
77 | const [region, changeRegion] = useState({ region: '' });
78 |
79 | const regionOptions = awsRegions.map((region) => (
80 |
83 | ));
84 |
85 | const handleRegionChange = (event) => {
86 | changeRegion({
87 | ...region,
88 | region: event.target.value,
89 | });
90 | };
91 |
92 | return (
93 |
94 |
95 |
96 |
97 |
98 | {/*
99 |
100 | */}
101 | {/*
102 | Pelican
103 | */}
104 |

110 |
191 |
192 |
193 |
194 | );
195 | }
196 |
197 | export default connect(null, mapDispatchToProps)(SignInSide);
198 |
--------------------------------------------------------------------------------
/client/RefreshRoute.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { Route, Redirect } from 'react-router-dom';
5 |
6 | const mapStateToProps = ({ clusterData }) => ({
7 | isDataAvailable: clusterData.isDataAvailable,
8 | });
9 |
10 | const RefreshRoute = ({
11 | component: Component,
12 | isDataAvailable,
13 | root,
14 | ...rest
15 | }) => (
16 |
19 | isDataAvailable ? :
20 | }
21 | />
22 | );
23 |
24 | export default connect(mapStateToProps)(RefreshRoute);
25 |
--------------------------------------------------------------------------------
/client/actions/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | export const setCredentials = (response) => ({
4 | type: types.SET_CREDENTIALS,
5 | payload: response,
6 | });
7 |
8 | export const getPods = (response) => ({
9 | type: types.GET_PODS,
10 | payload: response,
11 | });
12 |
13 | export const getNodes = (response) => ({
14 | type: types.GET_NODES,
15 | payload: response,
16 | });
17 |
18 | export const getDeployments = (response) => ({
19 | type: types.GET_DEPLOYMENTS,
20 | payload: response,
21 | });
22 |
23 | export const getServices = (response) => ({
24 | type: types.GET_SERVICES,
25 | payload: response,
26 | });
27 |
28 | export const getNamespaces = (response) => ({
29 | type: types.GET_NAMESPACES,
30 | payload: response,
31 | });
32 |
33 | export const firstLoad = () => ({
34 | type: types.FIRST_LOAD,
35 | });
36 |
37 | export const setDeployment = (response) => ({
38 | type: types.SET_DEPLOYMENT,
39 | payload: response,
40 | });
41 |
42 | export const setTargetNamespace = (response) => ({
43 | type: types.SET_TARGET_NAMESPACE,
44 | payload: response,
45 | });
46 |
--------------------------------------------------------------------------------
/client/assets/aws-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/pelican/e9195bde72e97bbc917252f5a779e9e0ce583a94/client/assets/aws-logo.png
--------------------------------------------------------------------------------
/client/assets/pelicanLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/pelican/e9195bde72e97bbc917252f5a779e9e0ce583a94/client/assets/pelicanLogo.png
--------------------------------------------------------------------------------
/client/components/Buttons/AddButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import AddCircleOutlineRoundedIcon from '@material-ui/icons/AddCircleOutlineRounded';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | root: {
8 | '& > *': {
9 | margin: '0px 0px 0px 0px',
10 | padding: '0px 0px 0px 0px',
11 | },
12 | },
13 | }));
14 |
15 | export default function AddButton({ onClick }) {
16 | const classes = useStyles();
17 |
18 | return (
19 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/client/components/Buttons/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import SettingsIcon from '@material-ui/icons/Settings';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | root: {
8 | '& > *': {
9 | margin: theme.spacing(1),
10 | },
11 | },
12 | }));
13 |
14 | export default function EditButton() {
15 | const classes = useStyles();
16 |
17 | return (
18 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/client/components/Buttons/CoolButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import IconButton from '@material-ui/core/IconButton';
4 | import SettingsIcon from '@material-ui/icons/Settings';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | root: {
8 | '& > *': {
9 | margin: theme.spacing(1),
10 | },
11 | },
12 | }));
13 |
14 | export default function EditButton() {
15 | const classes = useStyles();
16 |
17 | return (
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/client/components/Buttons/DeletePod.jsx:
--------------------------------------------------------------------------------
1 | import DeleteForeverOutlinedIcon from '@material-ui/icons/DeleteForeverOutlined';
2 | import IconButton from '@material-ui/core/IconButton';
3 |
4 | import React from 'react';
5 | import Button from '@material-ui/core/Button';
6 | import Dialog from '@material-ui/core/Dialog';
7 | import DialogActions from '@material-ui/core/DialogActions';
8 | import DialogContent from '@material-ui/core/DialogContent';
9 | import DialogContentText from '@material-ui/core/DialogContentText';
10 | import DialogTitle from '@material-ui/core/DialogTitle';
11 |
12 | export default function DeploymentModal({ onClick }) {
13 | const [open, setOpen] = React.useState(false);
14 |
15 | const handleClickOpen = () => {
16 | setOpen(true);
17 | };
18 |
19 | const handleClose = () => {
20 | setOpen(false);
21 | };
22 |
23 | return (
24 |
25 |
31 |
32 |
33 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/client/components/Buttons/DeploymentButton.jsx:
--------------------------------------------------------------------------------
1 | import AssignmentIcon from '@material-ui/icons/Assignment';
2 | import React from 'react';
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Button from '@material-ui/core/Button';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | root: {
8 | '& > *': {
9 | margin: theme.spacing(1),
10 | },
11 | },
12 | }));
13 |
14 | export default function DeploymentButton() {
15 | const classes = useStyles();
16 |
17 | return (
18 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/client/components/Buttons/DeploymentModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import Dialog from '@material-ui/core/Dialog';
4 | import DialogActions from '@material-ui/core/DialogActions';
5 | import DialogContent from '@material-ui/core/DialogContent';
6 | import DialogContentText from '@material-ui/core/DialogContentText';
7 | import DialogTitle from '@material-ui/core/DialogTitle';
8 | import AssignmentIcon from '@material-ui/icons/Assignment';
9 |
10 | import Radio from '@material-ui/core/Radio';
11 | import RadioGroup from '@material-ui/core/RadioGroup';
12 | import FormControlLabel from '@material-ui/core/FormControlLabel';
13 | import FormControl from '@material-ui/core/FormControl';
14 | import FormLabel from '@material-ui/core/FormLabel';
15 |
16 | export default function DeploymentModal({ newImage, oldImage, oldYaml }) {
17 | const [open, setOpen] = useState(false);
18 | const [value, setValue] = useState('standard');
19 | const [status, setStatus] = useState('Please select a deployment method');
20 |
21 | const handleClickOpen = () => {
22 | setOpen(true);
23 | };
24 |
25 | const handleClose = () => {
26 | setOpen(false);
27 | };
28 |
29 | const handleChange = (event) => {
30 | setValue(event.target.value);
31 | };
32 |
33 | const handleDeploy = async (
34 | newImage,
35 | oldImage,
36 | oldYaml,
37 | value,
38 | targetNamespace
39 | ) => {
40 | if (!targetNamespace || targetNamespace === 'All') {
41 | targetNamespace = JSON.parse(oldYaml).metadata.namespace;
42 | }
43 | if (value === 'blueGreen') {
44 | try {
45 | // create the new deployment with the new images
46 | setStatus('Creating the green deployment...');
47 | const result = await fetch('/api/deployments/bluegreen', {
48 | method: 'POST',
49 | headers: {
50 | 'Content-Type': 'application/json',
51 | },
52 | body: JSON.stringify({
53 | oldYaml: JSON.parse(oldYaml),
54 | newImage,
55 | targetNamespace,
56 | }),
57 | });
58 |
59 | if (result.status === 200) {
60 | setStatus('Successfully created the green deployment');
61 | const body = await result.json();
62 | const { greenDeploymentName, podSelectors } = body;
63 | setStatus('Giving green deployment 10 seconds to deploy...');
64 | setTimeout(async () => {
65 | // after 10 seconds, check to see if the there are as many pods as there should be
66 | const deploymentOk = await (
67 | await fetch(
68 | `/api/deployments/?name=${greenDeploymentName}&namespace=${targetNamespace}`
69 | )
70 | ).json();
71 | // if they are equal, then get the service that matches the old deployments pod's labels and we're going to switch its labels to the new deployments pods labels
72 | if (
73 | deploymentOk.status.availableReplicas ===
74 | deploymentOk.spec.replicas
75 | ) {
76 | setStatus('Green deployment has desired replicasets!');
77 | const serviceResult = await fetch(
78 | `/api/services/?namespace=${targetNamespace}`
79 | );
80 | const services = await serviceResult.json();
81 | const service = services.filter(
82 | (service) =>
83 | JSON.stringify(service.spec.selector) ==
84 | JSON.stringify(
85 | JSON.parse(oldYaml).spec.template.metadata.labels
86 | )
87 | )[0];
88 | setStatus('Switching over the load balancer...');
89 | const editServiceResult = await fetch(
90 | `/api/services/?name=${service.metadata.name}`,
91 | {
92 | method: 'PUT',
93 | headers: {
94 | 'Content-Type': 'application/json',
95 | },
96 | body: JSON.stringify({
97 | namespace: targetNamespace,
98 | config: deploymentOk.spec.template.metadata.labels,
99 | deployment: true,
100 | }),
101 | }
102 | );
103 | setStatus('Loadbalancer switched. Application Deployed!');
104 | } else {
105 | setStatus('Deployment failed. Deleting green deployment');
106 | await fetch(
107 | `/api/deployments?name=${greenDeploymentName}&namespace=${targetNamespace}`,
108 | {
109 | method: 'DELETE',
110 | }
111 | );
112 | }
113 | }, 10000);
114 | }
115 | } catch (err) {
116 | console.log(err);
117 | }
118 | }
119 | if (value === 'canary') {
120 | try {
121 | // create the new deployment with the new images
122 | setStatus('Creating the canary deployment');
123 | const result = await fetch('/api/deployments/canary', {
124 | method: 'POST',
125 | headers: {
126 | 'Content-Type': 'application/json',
127 | },
128 | body: JSON.stringify({
129 | oldYaml: JSON.parse(oldYaml),
130 | newImage,
131 | targetNamespace,
132 | }),
133 | });
134 | const canaryDeploymentName = await result.json();
135 | // wait a certain amount of time and see if the deployment has one available pod
136 | setStatus('Giving the canary deployment 10 seconds of uptime');
137 | setTimeout(async () => {
138 | const canaryDeploymentOk = await (
139 | await fetch(
140 | `/api/deployments/?name=${canaryDeploymentName}&namespace=${targetNamespace}`
141 | )
142 | ).json();
143 | // if we have an available replica, rollout the new image to the old deployment and delete the canary deployment
144 | if (canaryDeploymentOk.status.availableReplicas === 1) {
145 | setStatus('Canary successful! Rolling out deployment...');
146 | const newConfig = JSON.parse(oldYaml);
147 | newConfig.spec.template.spec.containers[0].image = newImage;
148 |
149 | // begin rollout of new image
150 | const updateResult = await fetch(
151 | `/api/deployments/?name=${newConfig.metadata.name}`,
152 | {
153 | method: 'PUT',
154 | headers: {
155 | 'Content-Type': 'application/json',
156 | },
157 | body: JSON.stringify({ config: newConfig }),
158 | }
159 | );
160 | setStatus('The deployment has been updated!');
161 | try {
162 | await fetch(
163 | `/api/deployments?name=${canaryDeploymentName}&namespace=${targetNamespace}`,
164 | {
165 | method: 'DELETE',
166 | }
167 | );
168 | } catch (err) {
169 | console.log(`err deleting: ${err}`);
170 | }
171 | } else {
172 | setStatus('Canary failed...');
173 | await fetch(
174 | `/api/deployments?name=${canaryDeploymentName}&namespace=${targetNamespace}`,
175 | {
176 | method: 'DELETE',
177 | }
178 | );
179 | }
180 | }, 10000);
181 | } catch (err) {
182 | console.log(err);
183 | }
184 | }
185 | if (value === 'standard') {
186 | setStatus('Rolling out the new deployment...');
187 | const newConfig = JSON.parse(oldYaml);
188 | newConfig.spec.template.spec.containers[0].image = newImage;
189 | const updateResult = await fetch(
190 | `/api/deployments/?name=${newConfig.metadata.name}`,
191 | {
192 | method: 'PUT',
193 | headers: {
194 | 'Content-Type': 'application/json',
195 | },
196 | body: JSON.stringify({ config: newConfig }),
197 | }
198 | );
199 | setStatus('Deployment successfully deployed!');
200 | }
201 | };
202 |
203 | return (
204 |
205 |
213 |
264 |
265 | );
266 | }
267 |
--------------------------------------------------------------------------------
/client/components/Buttons/SubmitButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Redirect } from 'react-router-dom';
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import Button from '@material-ui/core/Button';
5 | import SettingsIcon from '@material-ui/icons/Settings';
6 |
7 | const useStyles = makeStyles((theme) => ({
8 | root: {
9 | '& > *': {
10 | margin: theme.spacing(1),
11 | },
12 | },
13 | }));
14 |
15 | const handleSubmit = async (
16 | type,
17 | modifiedYaml,
18 | setValidJSON,
19 | namespace,
20 | setRedirect
21 | ) => {
22 | try {
23 | JSON.parse(modifiedYaml);
24 | } catch (err) {
25 | setValidJSON(false);
26 | console.log(err);
27 | }
28 | try {
29 | const config = JSON.parse(modifiedYaml);
30 | const result = await fetch(`/api/${type}?name=${config.metadata.name}`, {
31 | method: 'PUT',
32 | headers: {
33 | 'Content-Type': 'application/json',
34 | },
35 | body: JSON.stringify({ config, namespace }),
36 | });
37 | if (result.status === 200) {
38 | setRedirect(true);
39 | }
40 | } catch (err) {
41 | console.log(`Couldn't update the ${type.slice(0, -1)}, ${err}`);
42 | }
43 | };
44 |
45 | export default function SubmitButton({ type, namespace }) {
46 | const classes = useStyles();
47 | const [validJSON, setValidJSON] = useState(true);
48 | const [redirect, setRedirect] = useState(false);
49 | return validJSON ? (
50 | redirect ? (
51 |
52 | ) : (
53 |
68 | )
69 | ) : (
70 | That wasn't valid JSON!
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/client/components/Buttons/SubtractButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import Button from '@material-ui/core/Button';
4 | import RemoveCircleOutlineRoundedIcon from '@material-ui/icons/RemoveCircleOutlineRounded';
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | root: {
8 | '& > *': {
9 | margin: theme.spacing(0),
10 | },
11 | },
12 | }));
13 |
14 | export default function SubtractButton({ onClick }) {
15 | const classes = useStyles();
16 |
17 | return (
18 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/client/components/Buttons/TrashButton.jsx:
--------------------------------------------------------------------------------
1 | import DeleteForeverOutlinedIcon from '@material-ui/icons/DeleteForeverOutlined';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import IconButton from '@material-ui/core/IconButton';
4 |
5 | import React from 'react';
6 | import Button from '@material-ui/core/Button';
7 | import Dialog from '@material-ui/core/Dialog';
8 | import DialogActions from '@material-ui/core/DialogActions';
9 | import DialogContent from '@material-ui/core/DialogContent';
10 | import DialogContentText from '@material-ui/core/DialogContentText';
11 | import DialogTitle from '@material-ui/core/DialogTitle';
12 |
13 | export default function TrashButton({ onClick }) {
14 | const [open, setOpen] = React.useState(false);
15 |
16 | const handleClickOpen = () => {
17 | setOpen(true);
18 | };
19 |
20 | const handleClose = () => {
21 | setOpen(false);
22 | };
23 | //const [value, setValue] = React.useState('standard');
24 |
25 | //const handleChange = (event) => {
26 | //setValue(event.target.value);
27 | // };
28 |
29 | return (
30 |
31 |
37 |
38 |
39 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/client/components/Configurations/DeploymentConfigurations.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | import React, { useEffect, useState } from 'react';
4 | import { useParams, Link, Redirect } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import syntaxHighlight from '../../utils/yamlSyntaxHighlight';
7 | import DeploymentModal from '../Buttons/DeploymentModal.jsx';
8 | import SubmitButton from '../Buttons/SubmitButton.jsx';
9 | import FormFields from './ImagesForm.jsx';
10 | import Button from '@material-ui/core/Button';
11 |
12 | const mapStateToProps = ({ clusterData }) => ({
13 | clusterData,
14 | context: clusterData.context,
15 | targetNamespace: clusterData.targetNamespace,
16 | });
17 |
18 | function DeploymentConfiguration({ clusterData, context, targetNamespace }) {
19 | const [redirect, setRedirect] = useState(false);
20 | const [newImage, setNewImage] = useState('');
21 | const [yaml, setYaml] = useState('');
22 | const { name } = useParams();
23 |
24 | const objList = clusterData[context];
25 | const obj = objList.filter((objects) => objects.metadata.name === name)[0];
26 | const currentYaml = JSON.stringify(obj, null, 4);
27 | const editObj = { ...obj };
28 | delete editObj.status;
29 | const editYaml = JSON.stringify(editObj, null, 4);
30 |
31 | const [containers, setContainers] = useState(
32 | obj.spec.template.spec.containers
33 | );
34 |
35 | const handleClick = (e) => {
36 | e.target.style.height = 'inherit';
37 | e.target.style.height = `${e.target.scrollHeight}px`;
38 | };
39 |
40 | useEffect(() => {
41 | setYaml(
42 | (document.querySelector('#currentYaml').innerHTML = syntaxHighlight(
43 | currentYaml
44 | ))
45 | );
46 | }, []);
47 |
48 | return redirect ? (
49 |
50 | ) : (
51 |
58 |
76 |
77 | {`${context[0]
78 | .toUpperCase()
79 | .concat(context.slice(1, context.length - 1))} name: ${name}`}
80 |
81 |
Images:
{' '}
82 | {containers.map((container, i) => (
83 |
90 | ))}
91 |
97 |
98 |
110 |
111 |
Current Configuration:
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
119 | export default connect(mapStateToProps)(DeploymentConfiguration);
120 |
--------------------------------------------------------------------------------
/client/components/Configurations/ImagesForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import TextField from '@material-ui/core/TextField';
4 |
5 | const useStyles = makeStyles((theme) => ({
6 | root: {
7 | '& > *': {
8 | margin: theme.spacing(1),
9 | width: '25ch',
10 | },
11 | },
12 | }));
13 |
14 | export default function FormFields({ value, setNewImage, imgName }) {
15 | const classes = useStyles();
16 | const handleChange = (event) => {
17 | setNewImage(event.target.value);
18 | };
19 |
20 | return (
21 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/client/components/Configurations/NodeConfiguration.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | import React, { useEffect, useState } from 'react';
4 | import { useParams, Link, Redirect } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import syntaxHighlight from '../../utils/yamlSyntaxHighlight';
7 | import SubmitButton from '../Buttons/SubmitButton.jsx';
8 | import Button from '@material-ui/core/Button';
9 |
10 | const mapStateToProps = ({ clusterData }) => ({
11 | clusterData,
12 | context: clusterData.context,
13 | });
14 |
15 | function NodeConfiguration({ clusterData, context }) {
16 | const [redirect, setRedirect] = useState(false);
17 | const { name } = useParams();
18 |
19 | const objList = clusterData[context];
20 | const obj = objList.filter((objects) => objects.metadata.name === name)[0];
21 | const currentYaml = JSON.stringify(obj, null, 4);
22 | const editObj = { ...obj };
23 | delete editObj.status;
24 | const editYaml = JSON.stringify(editObj, null, 4);
25 |
26 | const handleClick = (e) => {
27 | e.target.style.height = 'inherit';
28 | e.target.style.height = `${e.target.scrollHeight}px`;
29 | };
30 |
31 | useEffect(() => {
32 | document.querySelector('#currentYaml').innerHTML = syntaxHighlight(
33 | currentYaml
34 | );
35 | }, []);
36 |
37 | return redirect ? (
38 |
39 | ) : (
40 |
47 |
65 |
66 | {`${context[0]
67 | .toUpperCase()
68 | .concat(context.slice(1, context.length - 1))} name: ${name}`}
69 |
70 |
71 |
81 |
82 |
Current Configuration:
83 |
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | export default connect(mapStateToProps)(NodeConfiguration);
91 |
--------------------------------------------------------------------------------
/client/components/Configurations/PodConfiguration.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | import React, { useEffect, useState } from 'react';
4 | import { useParams, Link, Redirect } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import syntaxHighlight from '../../utils/yamlSyntaxHighlight';
7 | import DeploymentButton from '../Buttons/DeploymentModal.jsx';
8 | import SubmitButton from '../Buttons/SubmitButton.jsx';
9 | import FormFields from './ImagesForm.jsx';
10 | import Button from '@material-ui/core/Button';
11 |
12 | const mapStateToProps = ({ clusterData }) => ({
13 | clusterData,
14 | context: clusterData.context,
15 | targetNamespace: clusterData.targetNamespace,
16 | });
17 |
18 | function PodConfiguration({ clusterData, context, targetNamespace }) {
19 | const [redirect, setRedirect] = useState(false);
20 | const { name } = useParams();
21 |
22 | const objList = clusterData[context];
23 | const obj = objList.filter((objects) => objects.metadata.name === name)[0];
24 | const currentYaml = JSON.stringify(obj, null, 4);
25 | const editObj = { ...obj };
26 | delete editObj.status;
27 | const editYaml = JSON.stringify(editObj, null, 4);
28 |
29 | const containers = obj.spec.containers;
30 |
31 | const handleClick = (e) => {
32 | e.target.style.height = 'inherit';
33 | e.target.style.height = `${e.target.scrollHeight}px`;
34 | };
35 |
36 | useEffect(() => {
37 | document.querySelector('#currentYaml').innerHTML = syntaxHighlight(
38 | currentYaml
39 | );
40 | }, []);
41 |
42 | return redirect ? (
43 |
44 | ) : (
45 |
52 |
70 |
71 | {`${context[0]
72 | .toUpperCase()
73 | .concat(context.slice(1, context.length - 1))} name: ${name}`}
74 |
75 |
76 |
85 |
86 |
Current Configuration:
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
94 | export default connect(mapStateToProps)(PodConfiguration);
95 |
--------------------------------------------------------------------------------
/client/components/Configurations/SelectorsForm.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import TextField from '@material-ui/core/TextField';
4 |
5 | const useStyles = makeStyles((theme) => ({
6 | root: {
7 | '& > *': {
8 | margin: theme.spacing(1),
9 | width: '25ch',
10 | },
11 | },
12 | }));
13 |
14 | export default function SelectorsForm({
15 | newKey,
16 | setNewKey,
17 | value1,
18 | value2,
19 | index,
20 | }) {
21 | const classes = useStyles();
22 | const handleChangeValue = (event) => {
23 | const obj = {};
24 | obj[value1] = event.target.value;
25 | const newObj = JSON.parse(JSON.stringify(newKey));
26 | newObj[index] = obj;
27 | setNewKey(newObj);
28 | };
29 | const handleChangeKey = (event) => {
30 | const obj = {};
31 | obj[event.target.value] = value2;
32 | const newObj = JSON.parse(JSON.stringify(newKey));
33 | newObj[index] = obj;
34 | setNewKey(newObj);
35 | };
36 |
37 | return (
38 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/client/components/Configurations/ServicesCongifuration.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | import React, { useEffect, useState } from 'react';
4 | import { useParams, Link, Redirect } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import syntaxHighlight from '../../utils/yamlSyntaxHighlight';
7 | import DeploymentButton from '../Buttons/DeploymentModal.jsx';
8 | import SubmitButton from '../Buttons/SubmitButton.jsx';
9 | import FormFields from './SelectorsForm.jsx';
10 | import Button from '@material-ui/core/Button';
11 |
12 | const mapStateToProps = ({ clusterData }) => ({
13 | clusterData,
14 | context: clusterData.context,
15 | targetNamespace: clusterData.targetNamespace,
16 | });
17 |
18 | function ServicesConfiguration({ clusterData, context, targetNamespace }) {
19 | const [redirect, setRedirect] = useState(false);
20 | const { name } = useParams();
21 |
22 | const objList = clusterData[context];
23 | const obj = objList.filter((objects) => objects.metadata.name === name)[0];
24 | const currentYaml = JSON.stringify(obj, null, 4);
25 | const editObj = { ...obj };
26 | delete editObj.status;
27 | const editYaml = JSON.stringify(editObj, null, 4);
28 |
29 | const specObj = clusterData[context].filter(
30 | (objects) => objects.metadata.name === name
31 | )[0];
32 | const selectObj = specObj.spec.selector;
33 | //map keys and values onto the form
34 | const keyArray = Object.keys(selectObj).map((key) => {
35 | const obj = {};
36 | obj[key] = selectObj[key];
37 | return obj;
38 | });
39 | const [newKey, setNewKey] = useState(keyArray);
40 |
41 | const handleClick = (e) => {
42 | e.target.style.height = 'inherit';
43 | e.target.style.height = `${e.target.scrollHeight}px`;
44 | };
45 | const handleSelectorChange = async () => {
46 | const newSelectors = {};
47 | for (let selector of newKey) {
48 | newSelectors[Object.keys(selector)[0]] =
49 | selector[Object.keys(selector)[0]];
50 | }
51 | try {
52 | const result = await fetch(`/api/services/?name=${name}`, {
53 | method: 'PUT',
54 | headers: {
55 | 'Content-Type': 'application/json',
56 | },
57 | body: JSON.stringify({
58 | config: { spec: { selector: newSelectors } },
59 | patch: true,
60 | namespace: targetNamespace,
61 | }),
62 | });
63 | if (result.status === 200) {
64 | setRedirect(true);
65 | }
66 | } catch (err) {
67 | console.log(
68 | `An error occured in trying to update the service ${name}: `,
69 | err
70 | );
71 | }
72 | };
73 |
74 | useEffect(() => {
75 | document.querySelector('#currentYaml').innerHTML = syntaxHighlight(
76 | currentYaml
77 | );
78 | }, []);
79 |
80 | return redirect ? (
81 |
82 | ) : (
83 |
90 |
108 |
109 | {`${context[0]
110 | .toUpperCase()
111 | .concat(context.slice(1, context.length - 1))} name: ${name}`}
112 |
113 |
114 |
123 |
124 |
Current Configuration:
125 |
126 |
127 |
128 |
129 | );
130 | }
131 |
132 | export default connect(mapStateToProps)(ServicesConfiguration);
133 |
--------------------------------------------------------------------------------
/client/components/NamespaceDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { connect } from 'react-redux';
3 | import { withStyles } from '@material-ui/core/styles';
4 | import Button from '@material-ui/core/Button';
5 | import Menu from '@material-ui/core/Menu';
6 | import MenuItem from '@material-ui/core/MenuItem';
7 | import ListItemIcon from '@material-ui/core/ListItemIcon';
8 | import ListItemText from '@material-ui/core/ListItemText';
9 | import * as actions from '../actions/actions';
10 |
11 | const StyledMenu = withStyles({
12 | paper: {
13 | border: '1px solid #d3d4d5',
14 | },
15 | })((props) => (
16 |
29 | ));
30 |
31 | const StyledMenuItem = withStyles((theme) => ({
32 | root: {
33 | '&:focus': {
34 | backgroundColor: theme.palette.primary.main,
35 | '& .MuiListItemIcon-root, & .MuiListItemText-primary': {
36 | color: theme.palette.common.white,
37 | },
38 | },
39 | },
40 | }))(MenuItem);
41 |
42 | const mapStateToProps = ({ clusterData }) => ({
43 | namespaces: clusterData.namespaces,
44 | });
45 |
46 | const mapDispatchToProps = (dispatch) => ({
47 | getNamespaces: (namespacesRes) =>
48 | dispatch(actions.getNamespaces(namespacesRes)),
49 | setTargetNamespace: (namespace) =>
50 | dispatch(actions.setTargetNamespace(namespace)),
51 | });
52 |
53 | function NamespaceDropdown({ getNamespaces, namespaces, setTargetNamespace }) {
54 | useEffect(() => {
55 | const fetchNamespaces = async () => {
56 | try {
57 | const response = await fetch('/api/namespaces');
58 | const namespacesRes = await response.json();
59 | getNamespaces(namespacesRes);
60 | } catch (err) {
61 | console.log('An error occured: ', err);
62 | }
63 | };
64 | fetchNamespaces();
65 | }, []);
66 |
67 | const [anchorEl, setAnchorEl] = useState(null);
68 |
69 | const handleClick = (event) => {
70 | setAnchorEl(event.currentTarget);
71 | };
72 |
73 | const handleClose = () => {
74 | setAnchorEl(null);
75 | };
76 |
77 | return (
78 |
79 |
88 |
109 |
110 | );
111 | }
112 |
113 | export default connect(mapStateToProps, mapDispatchToProps)(NamespaceDropdown);
114 |
--------------------------------------------------------------------------------
/client/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | import React, { useState } from 'react';
3 | import { Link } from 'react-router-dom';
4 | import AppBar from '@material-ui/core/AppBar';
5 | import CssBaseline from '@material-ui/core/CssBaseline';
6 | import Divider from '@material-ui/core/Divider';
7 | import Drawer from '@material-ui/core/Drawer';
8 | import Hidden from '@material-ui/core/Hidden';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import List from '@material-ui/core/List';
11 | import ListItem from '@material-ui/core/ListItem';
12 | import ListItemIcon from '@material-ui/core/ListItemIcon';
13 | import ListItemText from '@material-ui/core/ListItemText';
14 | import MenuIcon from '@material-ui/icons/Menu';
15 | import Toolbar from '@material-ui/core/Toolbar';
16 | import Typography from '@material-ui/core/Typography';
17 | import { makeStyles, useTheme } from '@material-ui/core/styles';
18 | import AccountTreeIcon from '@material-ui/icons/AccountTree';
19 | import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked';
20 | import BlurCircularSharpIcon from '@material-ui/icons/BlurCircularSharp';
21 | import PieChartRoundedIcon from '@material-ui/icons/PieChartRounded';
22 | import PeopleAltRoundedIcon from '@material-ui/icons/PeopleAltRounded';
23 | import * as actions from '../actions/actions';
24 | import NamespaceDropdown from './NamespaceDropdown.jsx';
25 |
26 | const drawerWidth = 200;
27 |
28 | const useStyles = makeStyles((theme) => ({
29 | drawer: {
30 | [theme.breakpoints.up('sm')]: {
31 | width: drawerWidth,
32 | flexShrink: 0,
33 | },
34 | },
35 | appBar: {
36 | [theme.breakpoints.up('sm')]: {
37 | width: '100%',
38 |
39 | position: 'fixed',
40 | zIndex: theme.zIndex.drawer + 1,
41 | },
42 | },
43 | menuButton: {
44 | marginRight: theme.spacing(2),
45 | [theme.breakpoints.up('sm')]: {
46 | display: 'none',
47 | },
48 | },
49 | toolbar: theme.mixins.toolbar,
50 | drawerPaper: {
51 | width: drawerWidth,
52 | },
53 | content: {
54 | flexGrow: 1,
55 | padding: theme.spacing(3),
56 | },
57 | }));
58 |
59 | function SideBar(props) {
60 | const { window } = props;
61 | const classes = useStyles();
62 | const theme = useTheme();
63 | const [mobileOpen, setMobileOpen] = useState(false);
64 | const handleDrawerToggle = () => {
65 | setMobileOpen(!mobileOpen);
66 | };
67 |
68 | function icons(index) {
69 | if (index === 0) return ;
70 | if (index === 1) return ;
71 | if (index === 2) return ;
72 | if (index === 3) return ;
73 | if (index === 4) return ;
74 | }
75 |
76 | const drawer = (
77 |
78 |
79 |
80 |
81 | {['Pods', 'Nodes', 'Deployments', 'Services'].map((text, index) => (
82 |
87 |
88 | {icons(index)}
89 |
90 |
91 |
92 | ))}
93 |
94 |
95 |
96 |
97 | );
98 |
99 | const container =
100 | window !== undefined ? () => window().document.body : undefined;
101 |
102 | return (
103 |
134 | );
135 | }
136 |
137 | export default SideBar;
138 |
139 | export function TopBar() {
140 | const classes = useStyles();
141 | const [mobileOpen, setMobileOpen] = useState(false);
142 | const handleDrawerToggle = () => {
143 | setMobileOpen(!mobileOpen);
144 | };
145 | return (
146 |
147 |
148 |
149 |
150 |
157 |
158 |
159 |
160 | Pelican
161 |
162 |
163 |
164 |
165 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------
/client/components/deployments/DeploymentRow.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import React from 'react';
3 | import { withStyles, makeStyles } from '@material-ui/core/styles';
4 | import Box from '@material-ui/core/Box';
5 | import Collapse from '@material-ui/core/Collapse';
6 | import IconButton from '@material-ui/core/IconButton';
7 | import Table from '@material-ui/core/Table';
8 | import TableBody from '@material-ui/core/TableBody';
9 | import TableCell from '@material-ui/core/TableCell';
10 | import TableContainer from '@material-ui/core/TableContainer';
11 | import TableHead from '@material-ui/core/TableHead';
12 | import TableRow from '@material-ui/core/TableRow';
13 | import Typography from '@material-ui/core/Typography';
14 | import Paper from '@material-ui/core/Paper';
15 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
16 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
17 | import tableTemplate from '../../constants/tableInfoTemplate';
18 | import { Link } from 'react-router-dom';
19 | import EditButton from '../Buttons/CoolButton.jsx';
20 | import AddButton from '../Buttons/AddButton.jsx';
21 | import SubtractButton from '../Buttons/SubtractButton.jsx';
22 | import DeleteButton from '../Buttons/TrashButton.jsx';
23 | import { trackPromise } from 'react-promise-tracker';
24 |
25 | const StyledTableCell = withStyles((theme) => ({
26 | head: {
27 | backgroundColor: theme.palette.common.black,
28 | color: theme.palette.common.white,
29 | },
30 | body: {
31 | fontSize: 14,
32 | },
33 | }))(TableCell);
34 |
35 | const StyledTableRow = withStyles((theme) => ({
36 | root: {
37 | '&:nth-of-type(odd)': {
38 | backgroundColor: theme.palette.action.hover,
39 | },
40 | },
41 | }))(TableRow);
42 | const useStyles = makeStyles({
43 | root: {
44 | '& > *': {
45 | borderBottom: 'unset',
46 | },
47 | },
48 | });
49 |
50 | const deleteDeployment = async (name, nameSpace, getDeployments) => {
51 | try {
52 | await fetch(`/api/deployments?name=${name}&namespace=${nameSpace}`, {
53 | method: 'DELETE',
54 | });
55 | setTimeout(async () => {
56 | await trackPromise(
57 | fetch('/api/deployments')
58 | .then((results) => results.json())
59 | .then((deployments) => getDeployments(deployments))
60 | );
61 | }, 1000);
62 | } catch (err) {
63 | console.log(err);
64 | }
65 | };
66 |
67 | const handleScale = (deployment, index, setDeployment, direction) => {
68 | let newNum = 0;
69 | if (direction === 'up') {
70 | newNum = deployment.spec.replicas + 1;
71 | } else {
72 | if (deployment.spec.replicas === 0) {
73 | return;
74 | }
75 | newNum = deployment.spec.replicas - 1;
76 | }
77 | fetch(`/api/deployments/scale?name=${deployment.metadata.name}`, {
78 | method: 'PUT',
79 | body: JSON.stringify({
80 | namespace: deployment.metadata.namespace,
81 | spec: { replicas: newNum },
82 | }),
83 | headers: {
84 | 'Content-Type': 'application/json',
85 | },
86 | })
87 | .then((result) => {
88 | return result.json();
89 | })
90 | .then((deployment) => {
91 | setDeployment({ deployment, index });
92 | })
93 | .catch((err) => console.log(err));
94 | };
95 |
96 | function DeploymentRow({ deployment, setDeployment, index, getDeployments }) {
97 | const [open, setOpen] = React.useState(false);
98 | const classes = useStyles();
99 |
100 | const cells = tableTemplate.deployments.columns.map((column, i) => {
101 | let property = { ...deployment };
102 | const splitArray = column.split('.');
103 | while (splitArray.length) {
104 | property = property[splitArray[0]];
105 | splitArray.shift();
106 | }
107 | if (column === 'spec.replicas') {
108 | return (
109 |
114 | {
116 | handleScale(deployment, index, setDeployment, 'down');
117 | }}
118 | />
119 | {property}
120 | handleScale(deployment, index, setDeployment, 'up')}
122 | />
123 |
124 | );
125 | } else {
126 | return (
127 |
128 | {property}
129 |
130 | );
131 | }
132 | });
133 |
134 | return (
135 | <>
136 |
137 |
138 | setOpen(!open)}
142 | >
143 | {open ? : }
144 |
145 |
146 |
147 | {deployment.metadata.name}
148 |
149 | {cells}
150 |
151 |
152 |
153 |
154 |
155 |
156 |
158 | deleteDeployment(
159 | deployment.metadata.name,
160 | deployment.metadata.namespace,
161 | getDeployments
162 | )
163 | }
164 | />
165 |
166 |
167 |
168 |
172 |
173 |
174 |
175 | Match Labels on Pods:
176 |
177 |
178 |
179 |
180 | Key
181 |
182 | Value
183 |
184 |
185 |
186 |
187 | {Object.keys(deployment.spec.selector.matchLabels).map(
188 | (key, i) => (
189 |
190 |
191 | {key}
192 |
193 |
194 | {deployment.spec.selector.matchLabels[key]}
195 |
196 |
197 | )
198 | )}
199 |
200 |
201 |
202 | Containers:
203 |
204 |
205 |
206 |
207 | Name
208 |
209 | Image
210 |
211 |
212 |
213 |
214 | {deployment.spec.template.spec.containers.map(
215 | (container, i) => (
216 |
217 |
218 | {container.name}
219 |
220 | {container.image}
221 |
222 | )
223 | )}
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 | >
232 | );
233 | }
234 |
235 | export default DeploymentRow;
236 |
--------------------------------------------------------------------------------
/client/components/deployments/DeploymentTable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | /* eslint-disable no-restricted-syntax */
4 | import React, { Component } from 'react';
5 | import { connect } from 'react-redux';
6 | import { makeStyles } from '@material-ui/core/styles';
7 | import Box from '@material-ui/core/Box';
8 | import Collapse from '@material-ui/core/Collapse';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Table from '@material-ui/core/Table';
11 | import TableBody from '@material-ui/core/TableBody';
12 | import TableCell from '@material-ui/core/TableCell';
13 | import TableContainer from '@material-ui/core/TableContainer';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import Typography from '@material-ui/core/Typography';
17 | import Paper from '@material-ui/core/Paper';
18 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
19 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
20 | import * as actions from '../../actions/actions';
21 | import Row from './DeploymentRow.jsx';
22 | import tableTemplate from '../../constants/tableInfoTemplate';
23 | import { trackPromise } from 'react-promise-tracker';
24 |
25 | const mapStateToProps = ({ clusterData }) => ({
26 | deployments: clusterData.deployments,
27 | targetNamespace: clusterData.targetNamespace,
28 | });
29 |
30 | const mapDispatchToProps = (dispatch) => ({
31 | getDeployments: (deployments) =>
32 | dispatch(actions.getDeployments(deployments)),
33 | setDeployment: (deployment, index) => {
34 | dispatch(actions.setDeployment(deployment, index));
35 | },
36 | firstLoad: () => dispatch(actions.firstLoad()),
37 | });
38 |
39 | class DeploymentTable extends Component {
40 | async componentDidMount() {
41 | const { getDeployments, firstLoad } = this.props;
42 | try {
43 | await trackPromise(
44 | fetch('/api/deployments')
45 | .then((results) => results.json())
46 | .then((deployments) => getDeployments(deployments))
47 | );
48 | firstLoad();
49 | } catch (err) {
50 | console.log('An error occured: ', err);
51 | }
52 | }
53 |
54 | render() {
55 | const {
56 | deployments,
57 | setDeployment,
58 | targetNamespace,
59 | getDeployments,
60 | } = this.props;
61 | const headers = tableTemplate.deployments.headers.map((header, i) => {
62 | return (
63 |
64 | {header}
65 |
66 | );
67 | });
68 | return (
69 |
77 |
78 |
79 |
80 | Deployments
81 | {headers}
82 |
83 |
84 |
85 | {deployments
86 | .filter((deployment) =>
87 | targetNamespace
88 | ? deployment.metadata.namespace === targetNamespace ||
89 | targetNamespace === 'All'
90 | : deployment
91 | )
92 | .map((deployment, i) => (
93 |
100 | ))}
101 |
102 |
103 |
104 | );
105 | }
106 | }
107 |
108 | export default connect(mapStateToProps, mapDispatchToProps)(DeploymentTable);
109 |
--------------------------------------------------------------------------------
/client/components/nodes/NodeRow.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | /* eslint-disable no-restricted-syntax */
3 | import React from 'react';
4 | import { withStyles, makeStyles } from '@material-ui/core/styles';
5 | import Box from '@material-ui/core/Box';
6 | import Collapse from '@material-ui/core/Collapse';
7 | import IconButton from '@material-ui/core/IconButton';
8 | import Table from '@material-ui/core/Table';
9 | import TableBody from '@material-ui/core/TableBody';
10 | import TableCell from '@material-ui/core/TableCell';
11 | import TableContainer from '@material-ui/core/TableContainer';
12 | import TableHead from '@material-ui/core/TableHead';
13 | import TableRow from '@material-ui/core/TableRow';
14 | import Typography from '@material-ui/core/Typography';
15 | import Paper from '@material-ui/core/Paper';
16 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
17 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
18 | import tableTemplate from '../../constants/tableInfoTemplate';
19 | import { Link } from 'react-router-dom';
20 | import EditButton from '../Buttons/CoolButton.jsx';
21 |
22 | const StyledTableCell = withStyles((theme) => ({
23 | head: {
24 | backgroundColor: theme.palette.common.black,
25 | color: theme.palette.common.white,
26 | },
27 | body: {
28 | fontSize: 14,
29 | },
30 | }))(TableCell);
31 |
32 | const StyledTableRow = withStyles((theme) => ({
33 | root: {
34 | '&:nth-of-type(odd)': {
35 | backgroundColor: theme.palette.action.hover,
36 | },
37 | },
38 | }))(TableRow);
39 | const useStyles = makeStyles({
40 | root: {
41 | '& > *': {
42 | borderBottom: 'unset',
43 | },
44 | },
45 | });
46 |
47 | function NodeRow({ node }) {
48 | const [open, setOpen] = React.useState(false);
49 | const classes = useStyles();
50 |
51 | const cells = tableTemplate.nodes.columns.map((column, i) => {
52 | let property = { ...node };
53 | const splitArray = column.split('.');
54 | while (splitArray.length) {
55 | property = property[splitArray[0]];
56 | splitArray.shift();
57 | }
58 | return (
59 |
60 | {property}
61 |
62 | );
63 | });
64 | return (
65 | <>
66 |
67 |
68 | setOpen(!open)}
72 | >
73 | {open ? : }
74 |
75 |
76 |
77 | {node.metadata.name}
78 |
79 | {cells}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
93 |
94 | Node Status:
95 |
96 |
97 |
98 |
99 | Status
100 |
101 | Condition
102 |
103 |
104 |
105 |
106 | {node.status.conditions.map((condition, i) => (
107 |
108 |
109 | {condition.type.split(/(?=[A-Z])/g).join(' ')}
110 |
111 | {condition.message}
112 |
113 | ))}
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | >
122 | );
123 | }
124 |
125 | export default NodeRow;
126 |
--------------------------------------------------------------------------------
/client/components/nodes/NodeTable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | /* eslint-disable no-restricted-syntax */
4 | import React, { Component } from 'react';
5 | import { connect } from 'react-redux';
6 | import { makeStyles } from '@material-ui/core/styles';
7 | import Box from '@material-ui/core/Box';
8 | import Collapse from '@material-ui/core/Collapse';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Table from '@material-ui/core/Table';
11 | import TableBody from '@material-ui/core/TableBody';
12 | import TableCell from '@material-ui/core/TableCell';
13 | import TableContainer from '@material-ui/core/TableContainer';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import Typography from '@material-ui/core/Typography';
17 | import Paper from '@material-ui/core/Paper';
18 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
19 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
20 | import * as actions from '../../actions/actions';
21 | import Row from './NodeRow.jsx';
22 | import tableTemplate from '../../constants/tableInfoTemplate';
23 | import { trackPromise } from 'react-promise-tracker';
24 |
25 | const mapStateToProps = ({ clusterData }) => ({
26 | nodes: clusterData.nodes,
27 | });
28 |
29 | const mapDispatchToProps = (dispatch) => ({
30 | getNodes: (nodes) => dispatch(actions.getNodes(nodes)),
31 | firstLoad: () => dispatch(actions.firstLoad()),
32 | });
33 |
34 | class NodeTable extends Component {
35 | async componentDidMount() {
36 | const { getNodes, firstLoad } = this.props;
37 | try {
38 | await trackPromise(
39 | fetch('/api/nodes')
40 | .then((results) => results.json())
41 | .then((nodes) => getNodes(nodes))
42 | );
43 | firstLoad();
44 | } catch (err) {
45 | console.log('An error occured: ', err);
46 | }
47 | }
48 |
49 | render() {
50 | const { nodes } = this.props;
51 | const headers = tableTemplate.nodes.headers.map((header, i) => {
52 | return (
53 |
54 | {header}
55 |
56 | );
57 | });
58 | return (
59 |
67 |
68 |
69 |
70 | Nodes
71 | {headers}
72 |
73 |
74 |
75 |
76 | {nodes.map((node, i) => (
77 |
78 | ))}
79 |
80 |
81 |
82 | );
83 | }
84 | }
85 |
86 | export default connect(mapStateToProps, mapDispatchToProps)(NodeTable);
87 |
--------------------------------------------------------------------------------
/client/components/pods/PodRow.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | /* eslint-disable no-restricted-syntax */
3 | import React from 'react';
4 | import { withStyles, makeStyles } from '@material-ui/core/styles';
5 | import Box from '@material-ui/core/Box';
6 | import Collapse from '@material-ui/core/Collapse';
7 | import IconButton from '@material-ui/core/IconButton';
8 | import Table from '@material-ui/core/Table';
9 | import TableBody from '@material-ui/core/TableBody';
10 | import TableCell from '@material-ui/core/TableCell';
11 | import TableContainer from '@material-ui/core/TableContainer';
12 | import TableHead from '@material-ui/core/TableHead';
13 | import TableRow from '@material-ui/core/TableRow';
14 | import Typography from '@material-ui/core/Typography';
15 | import Paper from '@material-ui/core/Paper';
16 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
17 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
18 | import tableTemplate from '../../constants/tableInfoTemplate';
19 | import { Link } from 'react-router-dom';
20 | import EditButton from '../Buttons/CoolButton.jsx';
21 | import DeleteButton from '../Buttons/DeletePod.jsx';
22 | import { trackPromise } from 'react-promise-tracker';
23 |
24 | const StyledTableCell = withStyles((theme) => ({
25 | head: {
26 | backgroundColor: theme.palette.common.black,
27 | color: theme.palette.common.white,
28 | },
29 | body: {
30 | fontSize: 14,
31 | },
32 | }))(TableCell);
33 |
34 | const StyledTableRow = withStyles((theme) => ({
35 | root: {
36 | '&:nth-of-type(odd)': {
37 | backgroundColor: theme.palette.action.hover,
38 | },
39 | },
40 | }))(TableRow);
41 | const useStyles = makeStyles({
42 | root: {
43 | '& > *': {
44 | borderBottom: 'unset',
45 | },
46 | },
47 | });
48 |
49 | const deletePod = async (name, nameSpace, getPods) => {
50 | try {
51 | await fetch(`/api/pods?name=${name}&namespace=${nameSpace}`, {
52 | method: 'DELETE',
53 | });
54 | setTimeout(async () => {
55 | await trackPromise(
56 | fetch('/api/pods')
57 | .then((results) => results.json())
58 | .then((pods) => getPods(pods))
59 | );
60 | }, 2000);
61 | } catch (err) {
62 | console.log(err);
63 | }
64 | };
65 |
66 | function Row({ pod, getPods }) {
67 | const [open, setOpen] = React.useState(false);
68 | const classes = useStyles();
69 |
70 | const cells = tableTemplate.pods.columns.map((column, i) => {
71 | if (column === 'Cpu') {
72 | return (
73 |
74 | {getCpu(pod)}
75 |
76 | );
77 | }
78 | if (column === 'Memory') {
79 | return (
80 |
81 | {getMemory(pod)}
82 |
83 | );
84 | }
85 | let property = { ...pod };
86 | const splitArray = column.split('.');
87 | while (splitArray.length) {
88 | property = property[splitArray[0]];
89 | splitArray.shift();
90 | }
91 | return (
92 |
93 | {property}
94 |
95 | );
96 | });
97 |
98 | function getCpu(pod) {
99 | return pod.spec.containers
100 | .map((container) =>
101 | container.resources.requests
102 | ? Number(
103 | container.resources.requests.cpu.substring(
104 | 0,
105 | container.resources.requests.cpu.length - 1
106 | )
107 | )
108 | : null
109 | )
110 | .reduce((curCpu, totalCpu) => {
111 | return (totalCpu += curCpu);
112 | });
113 | }
114 |
115 | function getMemory(pod) {
116 | return pod.spec.containers
117 | .map((container) =>
118 | container.resources.memory
119 | ? Number(
120 | container.resources.requests.memory.substring(
121 | 0,
122 | container.resources.requests.memory.length - 2
123 | )
124 | )
125 | : null
126 | )
127 | .reduce((curMem, totalMem) => {
128 | return (totalMem += curMem);
129 | });
130 | }
131 |
132 | return (
133 | <>
134 |
135 |
136 | setOpen(!open)}
140 | >
141 | {open ? : }
142 |
143 |
144 |
149 | {pod.metadata.name}
150 |
151 | {cells}
152 |
153 |
154 |
155 |
156 |
157 |
158 | {
160 | deletePod(pod.metadata.name, pod.metadata.namespace, getPods);
161 | }}
162 | />
163 |
164 |
165 |
166 |
170 |
171 |
172 |
173 | Status History:
174 |
175 |
176 |
177 |
178 | Status
179 |
180 | Transitioned At
181 |
182 |
183 |
184 |
185 | {pod.status.conditions.map((condition, i) => (
186 |
187 |
188 | {condition.type}
189 |
190 |
191 | {condition.lastTransitionTime}
192 |
193 |
194 | ))}
195 |
196 |
197 |
198 | Container Statuses:
199 |
200 |
201 |
202 |
203 | Name
204 |
205 | Image
206 |
207 |
208 | State
209 |
210 |
211 | Ready
212 |
213 |
214 | Restart Count
215 |
216 |
217 |
218 |
219 | {pod.status.containerStatuses.map((container, i) => (
220 |
221 |
222 | {container.name}
223 |
224 | {container.image}
225 |
226 | {Object.keys(container.state)[0]}
227 |
228 |
229 | {container.ready.toString()}
230 |
231 |
232 | {container.restartCount}
233 |
234 |
235 | ))}
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 | >
244 | );
245 | }
246 |
247 | export default Row;
248 |
--------------------------------------------------------------------------------
/client/components/pods/PodTable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | /* eslint-disable no-restricted-syntax */
4 | import React, { Component } from 'react';
5 | import { connect } from 'react-redux';
6 | import { makeStyles } from '@material-ui/core/styles';
7 | import Box from '@material-ui/core/Box';
8 | import Collapse from '@material-ui/core/Collapse';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Table from '@material-ui/core/Table';
11 | import TableBody from '@material-ui/core/TableBody';
12 | import TableCell from '@material-ui/core/TableCell';
13 | import TableContainer from '@material-ui/core/TableContainer';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import Typography from '@material-ui/core/Typography';
17 | import Paper from '@material-ui/core/Paper';
18 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
19 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
20 | import * as actions from '../../actions/actions';
21 | import Row from './PodRow.jsx';
22 | import tableTemplate from '../../constants/tableInfoTemplate';
23 | import { trackPromise } from 'react-promise-tracker';
24 |
25 | const mapStateToProps = ({ clusterData, appState }) => ({
26 | pods: clusterData.pods,
27 | firstLoad: appState.firstLoad,
28 | targetNamespace: clusterData.targetNamespace,
29 | });
30 |
31 | const mapDispatchToProps = (dispatch) => ({
32 | getPods: (pods) => dispatch(actions.getPods(pods)),
33 | firstLoad: () => dispatch(actions.firstLoad()),
34 | });
35 |
36 | class PodTable extends Component {
37 | async componentDidMount() {
38 | const { getPods, firstLoad } = this.props;
39 | try {
40 | await trackPromise(
41 | fetch('/api/pods')
42 | .then((results) => results.json())
43 | .then((pods) => getPods(pods))
44 | );
45 | firstLoad();
46 | } catch (err) {
47 | console.log('An error occured: ', err);
48 | }
49 | }
50 | render() {
51 | const { pods, targetNamespace, getPods } = this.props;
52 | const headers = tableTemplate.pods.headers.map((header, i) => {
53 | return (
54 |
55 | {header}
56 |
57 | );
58 | });
59 | return (
60 |
68 |
69 |
70 |
71 | Pods
72 | {headers}
73 |
74 |
75 |
76 |
77 | {pods
78 | .filter((pod) =>
79 | targetNamespace
80 | ? pod.metadata.namespace === targetNamespace ||
81 | targetNamespace === 'All'
82 | : pod
83 | )
84 | .map((pod, i) => (
85 |
86 | ))}
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default connect(mapStateToProps, mapDispatchToProps)(PodTable);
95 |
--------------------------------------------------------------------------------
/client/components/services/ServiceRow.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import React from 'react';
3 | import { withStyles, makeStyles } from '@material-ui/core/styles';
4 | import Box from '@material-ui/core/Box';
5 | import Collapse from '@material-ui/core/Collapse';
6 | import IconButton from '@material-ui/core/IconButton';
7 | import Table from '@material-ui/core/Table';
8 | import TableBody from '@material-ui/core/TableBody';
9 | import TableCell from '@material-ui/core/TableCell';
10 | import TableContainer from '@material-ui/core/TableContainer';
11 | import TableHead from '@material-ui/core/TableHead';
12 | import TableRow from '@material-ui/core/TableRow';
13 | import Typography from '@material-ui/core/Typography';
14 | import Paper from '@material-ui/core/Paper';
15 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
16 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
17 | import tableTemplate from '../../constants/tableInfoTemplate';
18 | import { Link } from 'react-router-dom';
19 | import EditButton from '../Buttons/CoolButton.jsx';
20 |
21 | const StyledTableCell = withStyles((theme) => ({
22 | head: {
23 | backgroundColor: theme.palette.common.black,
24 | color: theme.palette.common.white,
25 | },
26 | body: {
27 | fontSize: 14,
28 | },
29 | }))(TableCell);
30 |
31 | const StyledTableRow = withStyles((theme) => ({
32 | root: {
33 | '&:nth-of-type(odd)': {
34 | backgroundColor: theme.palette.action.hover,
35 | },
36 | },
37 | }))(TableRow);
38 | const useStyles = makeStyles({
39 | root: {
40 | '& > *': {
41 | borderBottom: 'unset',
42 | },
43 | },
44 | });
45 |
46 | function ServiceRow({ service }) {
47 | const [open, setOpen] = React.useState(false);
48 | const classes = useStyles();
49 |
50 | const cells = tableTemplate.services.columns.map((column, i) => {
51 | let property = { ...service };
52 | const splitArray = column.split('.');
53 | while (splitArray.length) {
54 | property = property[splitArray[0]];
55 | splitArray.shift();
56 | }
57 | return (
58 |
59 | {property}
60 |
61 | );
62 | });
63 |
64 | return (
65 | <>
66 |
67 |
68 | setOpen(!open)}
72 | >
73 | {open ? : }
74 |
75 |
76 |
77 | {service.metadata.name}
78 |
79 | {cells}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
93 |
94 | Match Labels on Pods:
95 |
96 |
97 |
98 |
99 | Key
100 |
101 | Value
102 |
103 |
104 |
105 |
106 | {service.spec.selector
107 | ? Object.keys(service.spec.selector).map((key, i) => (
108 |
109 |
110 | {key}
111 |
112 |
113 | {service.spec.selector[key]}
114 |
115 |
116 | ))
117 | : null}
118 |
119 |
120 |
121 | Ports:
122 |
123 |
124 |
125 |
126 |
127 | Protocol
128 |
129 | Port
130 |
131 | Target Port
132 |
133 |
134 | Node Port
135 |
136 |
137 |
138 |
139 | {service.spec.ports
140 | ? service.spec.ports.map((portObj, i) => (
141 |
142 | {Object.values(portObj).map((value, i) => (
143 |
144 | {value}
145 |
146 | ))}
147 |
148 | ))
149 | : null}
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | >
158 | );
159 | }
160 |
161 | export default ServiceRow;
162 |
--------------------------------------------------------------------------------
/client/components/services/ServiceTable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable react/prop-types */
3 | /* eslint-disable no-restricted-syntax */
4 | import React, { Component } from 'react';
5 | import { connect } from 'react-redux';
6 | import { makeStyles } from '@material-ui/core/styles';
7 | import Box from '@material-ui/core/Box';
8 | import Collapse from '@material-ui/core/Collapse';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Table from '@material-ui/core/Table';
11 | import TableBody from '@material-ui/core/TableBody';
12 | import TableCell from '@material-ui/core/TableCell';
13 | import TableContainer from '@material-ui/core/TableContainer';
14 | import TableHead from '@material-ui/core/TableHead';
15 | import TableRow from '@material-ui/core/TableRow';
16 | import Typography from '@material-ui/core/Typography';
17 | import Paper from '@material-ui/core/Paper';
18 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
19 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
20 | import * as actions from '../../actions/actions';
21 | import Row from './ServiceRow.jsx';
22 | import tableTemplate from '../../constants/tableInfoTemplate';
23 | import { trackPromise } from 'react-promise-tracker';
24 |
25 | const mapStateToProps = ({ clusterData }) => ({
26 | services: clusterData.services,
27 | targetNamespace: clusterData.targetNamespace,
28 | });
29 |
30 | const mapDispatchToProps = (dispatch) => ({
31 | getServices: (services) => dispatch(actions.getServices(services)),
32 | firstLoad: () => dispatch(actions.firstLoad()),
33 | });
34 |
35 | class ServiceTable extends Component {
36 | async componentDidMount() {
37 | const { getServices, firstLoad } = this.props;
38 | try {
39 | await trackPromise(
40 | fetch('/api/services')
41 | .then((results) => results.json())
42 | .then((services) => getServices(services))
43 | );
44 | firstLoad();
45 | } catch (err) {
46 | console.log('An error occured: ', err);
47 | }
48 | }
49 |
50 | render() {
51 | const { services, targetNamespace } = this.props;
52 | const headers = tableTemplate.services.headers.map((header, i) => {
53 | return (
54 |
55 | {header}
56 |
57 | );
58 | });
59 | return (
60 |
68 |
69 |
70 |
71 | Services
72 | {headers}
73 |
74 |
75 |
76 |
77 | {services
78 | .filter((service) =>
79 | targetNamespace
80 | ? service.metadata.namespace === targetNamespace ||
81 | targetNamespace === 'All'
82 | : service
83 | )
84 | .map((service, i) => (
85 |
86 | ))}
87 |
88 |
89 |
90 | );
91 | }
92 | }
93 |
94 | export default connect(mapStateToProps, mapDispatchToProps)(ServiceTable);
95 |
--------------------------------------------------------------------------------
/client/constants/actionTypes.js:
--------------------------------------------------------------------------------
1 | // list of action types exported as variables so that type-checking in reducers is easier for the developer
2 |
3 | export const SET_CREDENTIALS = 'SET_CREDENTIALS';
4 | export const GET_PODS = 'GET_PODS';
5 | export const GET_NODES = 'GET_NODES';
6 | export const GET_DEPLOYMENTS = 'GET_DEPLOYMENTS';
7 | export const GET_SERVICES = 'GET_SERVICES';
8 | export const GET_NAMESPACES = 'GET_NAMESPACES';
9 | export const FIRST_LOAD = 'FIRST_LOAD';
10 | export const SET_DEPLOYMENT = 'SET_DEPLOYMENT';
11 |
12 | export const SET_TARGET_NAMESPACE = 'SET_TARGET_NAMESPACE';
13 |
--------------------------------------------------------------------------------
/client/constants/awsRegions.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { code: 'us-east-2', name: 'US East (Ohio)' },
3 | { code: 'us-east-1', name: 'US East (N. Virginia)' },
4 | { code: 'us-west-1', name: 'US West (N. California)' },
5 | { code: 'us-west-2', name: 'US West (Oregon)' },
6 | { code: 'af-south-1', name: 'Africa (Cape Town)' },
7 | { code: 'ap-east-1', name: 'Asia Pacific (Hong Kong)' },
8 | { code: 'ap-south-1', name: 'Asia Pacific (Mumbai)' },
9 | { code: 'ap-northeast-3', name: 'Asia Pacific (Osaka-Local)' },
10 | { code: 'ap-northeast-2', name: 'Asia Pacific (Seoul)' },
11 | { code: 'ap-southeast-1', name: 'Asia Pacific (Singapore)' },
12 | { code: 'ap-southeast-2', name: 'Asia Pacific (Sydney)' },
13 | { code: 'ap-northeast-1', name: 'Asia Pacific (Tokyo)' },
14 | { code: 'ca-central-1', name: 'Canada (Central)' },
15 | { code: 'cn-north-1', name: 'China (Beijing)' },
16 | { code: 'cn-northwest-1', name: 'China (Ningxia)' },
17 | { code: 'eu-central-1', name: 'Europe (Frankfurt)' },
18 | { code: 'eu-west-1', name: 'Europe (Ireland)' },
19 | { code: 'eu-west-2', name: 'Europe (London)' },
20 | { code: 'eu-south-1', name: 'Europe (Milan)' },
21 | { code: 'eu-west-3', name: 'Europe (Paris)' },
22 | { code: 'eu-north-1', name: 'Europe (Stockholm)' },
23 | { code: 'me-south-1', name: 'Middle East (Bahrain)' },
24 | { code: 'sa-east-1', name: 'South America (Sao Paulo)' },
25 | ];
26 |
--------------------------------------------------------------------------------
/client/constants/tableInfoTemplate.js:
--------------------------------------------------------------------------------
1 | export default {
2 | pods: {
3 | headers: [
4 | 'Name',
5 | 'Status',
6 | 'Node',
7 | 'Pod IP',
8 | 'Creation Timestamp',
9 | 'Requested CPU (m)',
10 | 'Requested Memory (Mi)',
11 | 'Namespace',
12 | ' Edit',
13 | 'Delete',
14 | ],
15 | columns: [
16 | 'status.phase',
17 | 'spec.nodeName',
18 | 'status.podIP',
19 | 'metadata.creationTimestamp',
20 | 'Cpu',
21 | 'Memory',
22 | 'metadata.namespace',
23 | ],
24 | },
25 | nodes: {
26 | headers: [
27 | 'Name',
28 | 'Internal IP',
29 | 'External IP',
30 | 'Pod CIDR',
31 | 'Allocatable CPU',
32 | 'Capacity CPU',
33 | 'Creation Timestamp',
34 | ' Edit',
35 | ],
36 | columns: [
37 | 'status.addresses.0.address',
38 | 'status.addresses.1.address',
39 | 'spec.podCIDR',
40 | 'status.allocatable.cpu',
41 | 'status.capacity.cpu',
42 | 'metadata.creationTimestamp',
43 | ],
44 | },
45 | deployments: {
46 | headers: [
47 | 'Name',
48 | 'Namespace',
49 | 'Desired Replicas',
50 | 'Available Replicas',
51 | 'Updated Replicas',
52 | 'Strategy Type',
53 | ' Edit',
54 | 'Delete',
55 | ],
56 | columns: [
57 | 'metadata.namespace',
58 | 'spec.replicas',
59 | 'status.readyReplicas',
60 | 'status.updatedReplicas',
61 | 'spec.strategy.type',
62 | ],
63 | },
64 | services: {
65 | headers: [
66 | 'Name',
67 | 'Namespace',
68 | 'Cluster IP',
69 | 'Type',
70 | 'Creation Timestamp',
71 | ' Edit',
72 | ],
73 | columns: [
74 | 'metadata.namespace',
75 | 'spec.clusterIP',
76 | 'spec.type',
77 | 'metadata.creationTimestamp',
78 | ],
79 | },
80 | namespaces: {},
81 | };
82 |
--------------------------------------------------------------------------------
/client/containers/LoginContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route, Redirect } from 'react-router-dom';
3 | import LoginPage from '../LoginPage.jsx';
4 | import '../stylesheets/styles.scss';
5 |
6 | function LoginContainer() {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 |
14 |
15 | >
16 | );
17 | }
18 |
19 | export default LoginContainer;
20 |
--------------------------------------------------------------------------------
/client/containers/MainContainer.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/extensions */
2 | import React from 'react';
3 | import { connect } from 'react-redux';
4 | import { Switch, Route, Redirect } from 'react-router-dom';
5 | import SideBar, { TopBar } from '../components/Navbar.jsx';
6 | import PodTable from '../components/pods/PodTable.jsx';
7 | import NodeTable from '../components/nodes/NodeTable.jsx';
8 | import ServiceTable from '../components/services/ServiceTable.jsx';
9 | import DeploymentTable from '../components/deployments/DeploymentTable.jsx';
10 | import DeploymentConfiguration from '../components/Configurations/DeploymentConfigurations.jsx';
11 | import ServicesConfiguration from '../components/Configurations/ServicesCongifuration.jsx';
12 | import NodeConfiguration from '../components/Configurations/NodeConfiguration.jsx';
13 | import PodConfiguration from '../components/Configurations/PodConfiguration.jsx';
14 | import RefreshRoute from '../RefreshRoute.jsx';
15 |
16 | const mapStateToProps = ({ awsAuth }) => ({
17 | accessKeyId: awsAuth.accessKey,
18 | });
19 |
20 | function MainContainer() {
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
40 |
45 |
50 |
51 |
52 |
53 |
54 | >
55 | );
56 | }
57 |
58 | export default connect(mapStateToProps)(MainContainer);
59 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import store from './store';
5 | import App from './App.jsx';
6 | import LoadingIndicator from './LoadingIndicator.jsx';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 | ,
13 | document.querySelector('#root')
14 | );
15 |
--------------------------------------------------------------------------------
/client/reducers/appStateReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | const initialState = {
4 | firstLoad: true,
5 | };
6 |
7 | const appState = (state = initialState, action) => {
8 | switch (action.type) {
9 | case types.FIRST_LOAD:
10 | return {
11 | ...state,
12 | firstLoad: false,
13 | };
14 | default:
15 | return state;
16 | }
17 | };
18 |
19 | export default appState;
20 |
--------------------------------------------------------------------------------
/client/reducers/awsAuthReducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | const initialState = {
4 | accessKeyId: '',
5 | secretAccessKey: '',
6 | region: '',
7 | };
8 |
9 | const awsAuthReducer = (state = initialState, action) => {
10 | switch (action.type) {
11 | case types.SET_CREDENTIALS:
12 | return {
13 | ...state,
14 | accessKeyId: action.payload.accessKeyId,
15 | secretAccessKey: action.payload.secretAccessKey,
16 | region: action.payload.region,
17 | };
18 | default:
19 | return state;
20 | }
21 | };
22 |
23 | export default awsAuthReducer;
24 |
--------------------------------------------------------------------------------
/client/reducers/clusterData.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | const initialState = {
4 | pods: [],
5 | nodes: [],
6 | deployments: [],
7 | services: [],
8 | namespaces: [],
9 | context: 'pods',
10 | isDataAvailable: false,
11 | targetNamespace: '',
12 | };
13 |
14 | const clusterData = (state = initialState, action) => {
15 | switch (action.type) {
16 | case types.GET_PODS:
17 | return {
18 | ...state,
19 | pods: action.payload,
20 | context: 'pods',
21 | isDataAvailable: true,
22 | };
23 | case types.GET_NODES:
24 | return {
25 | ...state,
26 | nodes: action.payload,
27 | context: 'nodes',
28 | isDataAvailable: true,
29 | };
30 | case types.GET_DEPLOYMENTS:
31 | return {
32 | ...state,
33 | deployments: action.payload,
34 | context: 'deployments',
35 | isDataAvailable: true,
36 | };
37 | case types.GET_SERVICES:
38 | return {
39 | ...state,
40 | services: action.payload,
41 | context: 'services',
42 | isDataAvailable: true,
43 | };
44 | case types.SET_DEPLOYMENT:
45 | const newDeployments = JSON.parse(JSON.stringify(state.deployments));
46 | newDeployments[action.payload.index] = JSON.parse(
47 | JSON.stringify(action.payload.deployment)
48 | );
49 | return {
50 | ...state,
51 | deployments: newDeployments,
52 | };
53 | case types.GET_NAMESPACES:
54 | return {
55 | ...state,
56 | namespaces: action.payload,
57 | };
58 | case types.SET_TARGET_NAMESPACE:
59 | return {
60 | ...state,
61 | targetNamespace: action.payload,
62 | };
63 | default:
64 | return state;
65 | }
66 | };
67 |
68 | export default clusterData;
69 |
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import awsAuthReducer from './awsAuthReducer';
3 | import clusterData from './clusterData';
4 | import appState from './appStateReducer';
5 |
6 | const reducers = combineReducers({
7 | awsAuth: awsAuthReducer,
8 | clusterData: clusterData,
9 | appState: appState,
10 | });
11 |
12 | export default reducers;
13 |
--------------------------------------------------------------------------------
/client/store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension';
3 | import reducers from './reducers/index';
4 |
5 | const store = createStore(reducers, composeWithDevTools());
6 |
7 | export default store;
8 |
--------------------------------------------------------------------------------
/client/stylesheets/styles.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial;
3 | background-color: #f2f2f2;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | #main {
9 | display: grid;
10 | }
11 | //----------------------- STYLES RELATED TO CONFIG FILES------------------------------ //
12 |
13 | pre {
14 | outline: 1px solid #ccc;
15 | padding: 5px;
16 | margin: 5px;
17 | }
18 | .key {
19 | color: #00a0a0;
20 | }
21 |
22 | #configHeader {
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | }
27 |
28 | #configBtns {
29 | display: flex;
30 | }
31 |
32 | #backBtn,
33 | #submitBtn {
34 | height: 3.25em;
35 | width: 6em;
36 | color: white;
37 | font-size: 1em;
38 | font-weight: 600;
39 | border-radius: 5px;
40 | border: 1px solid white;
41 | background-color: #00a0a0;
42 | outline: none;
43 | }
44 |
45 | #backBtn:hover,
46 | #submitBtn:hover {
47 | cursor: pointer;
48 | }
49 |
50 | #yamlContainer {
51 | display: grid;
52 | grid-template-columns: 1fr 1fr;
53 | }
54 |
55 | #currentYaml {
56 | white-space: pre-wrap;
57 | color: whitesmoke;
58 | }
59 |
60 | #editYaml {
61 | white-space: pre-wrap;
62 | font-family: inherit;
63 | font-size: inherit;
64 | font-weight: inherit;
65 | line-height: inherit;
66 | letter-spacing: inherit;
67 | resize: none;
68 | width: 90%;
69 | height: 85%;
70 | border-radius: 5px;
71 | border: 3px solid #00a0a0;
72 | outline: none;
73 | margin-right: 40px;
74 | color: whitesmoke;
75 | background-color: rgb(45, 45, 45);
76 | box-shadow: 5px 5px 5px rgb(25, 25, 25);
77 | }
78 |
79 | //------------------------------------------------------------------------------- //
80 |
81 | #login-container {
82 | display: grid;
83 | grid-template-columns: 1.25fr 1fr 1.25fr;
84 | grid-template-rows: 20vh 43vh 37vh;
85 | grid-template-areas:
86 | '. . .'
87 | '. login .'
88 | '. . .';
89 | }
90 |
91 | #login {
92 | grid-area: login;
93 | background-color: white;
94 | border: 1px solid #a6a6a6;
95 | border-radius: 8px;
96 | box-shadow: 5px 10px 10px #bfbfbf;
97 | }
98 |
99 | #form {
100 | padding: 0.5em 3em;
101 | }
102 |
103 | form div {
104 | padding: 0;
105 | margin: 0;
106 | }
107 |
108 | #app-name h1 {
109 | padding: 0.5em;
110 | margin: 0;
111 | }
112 |
113 | #loading {
114 | display: flex;
115 | justify-content: center;
116 | }
117 | #loading > {
118 | margin: 5px 5px 5px 5px;
119 | padding: 5px 5px 5px 5px;
120 | }
121 |
--------------------------------------------------------------------------------
/client/utils/yamlSyntaxHighlight.js:
--------------------------------------------------------------------------------
1 | export default function syntaxHighlight(json) {
2 | json = json.replace(/&/g, '&').replace(//g, '>');
3 | return json.replace(
4 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
5 | function (match) {
6 | var cls = 'number';
7 | if (/^"/.test(match)) {
8 | if (/:$/.test(match)) {
9 | cls = 'key';
10 | } else {
11 | cls = 'string';
12 | }
13 | } else if (/true|false/.test(match)) {
14 | cls = 'boolean';
15 | } else if (/null/.test(match)) {
16 | cls = 'null';
17 | }
18 | return '' + match + '';
19 | }
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/k8s-client/config.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('kubernetes-client');
2 |
3 | // for now you must have a local cluster running for the backend to start
4 | // we will have to add auth and other try catches to allow program to run before a user has logged in
5 | module.exports = new Client({ version: '1.13' }); // used to be 1.13
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "finch",
3 | "version": "1.0.0",
4 | "description": "automated canary testing for kubernetes clusters",
5 | "main": "client/index.js",
6 | "scripts": {
7 | "build": "NODE_ENV=production webpack",
8 | "start": "NODE_ENV=production nodemon server/server.js",
9 | "dev": "NODE_ENV=development webpack-dev-server --open & NODE_ENV=production nodemon server/server.js",
10 | "test": "jest"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/KCarpen/finch.git"
15 | },
16 | "author": "CLL&S",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/KCarpen/finch/issues"
20 | },
21 | "homepage": "https://github.com/KCarpen/finch#readme",
22 | "dependencies": {
23 | "@material-ui/core": "^4.10.1",
24 | "@material-ui/icons": "^4.9.1",
25 | "express": "^4.17.1",
26 | "json-stream": "^1.0.0",
27 | "kubernetes-client": "^9.0.0",
28 | "node-sass": "^4.14.1",
29 | "nodemon": "^2.0.4",
30 | "path": "^0.12.7",
31 | "react": "^16.13.1",
32 | "react-bootstrap": "^1.0.1",
33 | "react-dom": "^16.13.1",
34 | "react-loader-spinner": "^3.1.14",
35 | "react-promise-tracker": "^2.1.0",
36 | "react-redux": "^7.2.0",
37 | "react-router-dom": "^5.2.0",
38 | "redux": "^4.0.5",
39 | "redux-devtools-extension": "^2.13.8",
40 | "sass": "^1.26.7",
41 | "url-loader": "^4.1.0",
42 | "webpack": "^4.43.0"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.10.2",
46 | "@babel/preset-env": "^7.10.2",
47 | "@babel/preset-react": "^7.10.1",
48 | "@kubernetes/client-node": "^0.12.0",
49 | "babel-eslint": "^10.1.0",
50 | "babel-loader": "^8.1.0",
51 | "babel-polyfill": "^6.26.0",
52 | "css-loader": "^3.5.3",
53 | "enzyme": "^3.11.0",
54 | "enzyme-adapter-react-16": "^1.15.2",
55 | "enzyme-to-json": "^3.5.0",
56 | "eslint": "^7.2.0",
57 | "eslint-config-airbnb": "^18.1.0",
58 | "eslint-config-prettier": "^6.11.0",
59 | "eslint-plugin-import": "^2.21.2",
60 | "eslint-plugin-jsx-a11y": "^6.2.3",
61 | "eslint-plugin-prettier": "^3.1.3",
62 | "eslint-plugin-react": "^7.20.0",
63 | "file-loader": "^6.0.0",
64 | "jest": "^26.0.1",
65 | "prettier": "^2.0.5",
66 | "sass-loader": "^8.0.2",
67 | "source-map-loader": "^1.0.0",
68 | "style-loader": "^1.2.1",
69 | "webpack-cli": "^3.3.11",
70 | "webpack-dev-server": "^3.11.0"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/server/controllers/DeploymentController.js:
--------------------------------------------------------------------------------
1 | const client = require('../kubernetes-config');
2 | const util = require('util');
3 |
4 | module.exports = {
5 | getDeployments: async (req, res, next) => {
6 | const { name, namespace } = req.query;
7 | if (name) {
8 | try {
9 | res.locals.deployments = (
10 | await client.apis.apps.v1
11 | .namespaces(namespace)
12 | .deployments(name)
13 | .get()
14 | ).body;
15 | next();
16 | } catch (err) {
17 | next({
18 | log: `Encountered an error in DeploymentController.get: ${err}`,
19 | status: 400,
20 | message: 'An error occured fetching deployments',
21 | });
22 | }
23 | } else {
24 | try {
25 | res.locals.deployments = (
26 | await client.apis.apps.v1.deployments.get()
27 | ).body.items;
28 | next();
29 | } catch (err) {
30 | next({
31 | log: `Encountered an error in DeploymentController.get: ${err}`,
32 | status: 400,
33 | message: 'An error occured fetching deployments',
34 | });
35 | }
36 | }
37 | },
38 | scaleDeployment: async (req, res, next) => {
39 | try {
40 | const { name } = req.query;
41 | const { spec } = req.body;
42 | const namespace = req.body.namespace || 'default';
43 | if (spec.replicas < 0) {
44 | throw new Error('Cannot set a negative replica');
45 | }
46 | res.locals.deployment = (
47 | await client.apis.apps.v1
48 | .namespaces(namespace)
49 | .deployments(name)
50 | .patch({ body: { spec } })
51 | ).body;
52 | next();
53 | } catch (err) {
54 | next({
55 | log: `Encountered an error in DeploymentController.scale: ${err}`,
56 | status: 500,
57 | message: 'An error occured scaling the deploymnet',
58 | });
59 | }
60 | },
61 | updateDeployment: async (req, res, next) => {
62 | const namespace = req.body.namespace || 'default';
63 | const { config } = req.body;
64 | try {
65 | await client.apis.apps.v1
66 | .namespaces(namespace)
67 | .deployments(req.query.name)
68 | .put({ body: config });
69 | next();
70 | } catch (err) {
71 | next({
72 | log: `Encountered an error in DeploymentController.update: ${err}`,
73 | status: 500,
74 | message: 'An error occured updating the deployment',
75 | });
76 | }
77 | },
78 | createGreenDeployment: async (req, res, next) => {
79 | const { newImage, oldYaml, targetNamespace } = req.body;
80 | try {
81 | // make deep copy of old deployment without metadata
82 | const greenDeployment = JSON.parse(
83 | JSON.stringify({
84 | spec: oldYaml.spec,
85 | metadata: {
86 | name: oldYaml.metadata.name,
87 | labels: oldYaml.metadata.labels,
88 | },
89 | })
90 | );
91 |
92 | // change name incase this is another green deployment
93 | const sliceIndex =
94 | greenDeployment.metadata.name.indexOf('-green') === -1
95 | ? greenDeployment.metadata.name.length
96 | : greenDeployment.metadata.name.indexOf('-green');
97 |
98 | // change name of green deployment
99 | greenDeployment.metadata.name =
100 | greenDeployment.metadata.name.slice(0, sliceIndex) +
101 | '-green' +
102 | Date.now().toString();
103 |
104 | // add new spec selector matchlabel to green deployment
105 | greenDeployment.spec.selector.matchLabels.greenVersion = Date.now().toString();
106 |
107 | // add that label to the pods themselves
108 | greenDeployment.spec.template.metadata.labels.greenVersion =
109 | greenDeployment.spec.selector.matchLabels.greenVersion;
110 |
111 | // change the image of the 0th container
112 | greenDeployment.spec.template.spec.containers[0].image = newImage;
113 |
114 | const newGreenDeployment = (
115 | await client.apis.apps.v1
116 | .namespaces(targetNamespace)
117 | .deployments.post({ body: greenDeployment })
118 | ).body;
119 |
120 | res.locals.greenDeploymentName = newGreenDeployment.metadata.name;
121 | res.locals.podSelector = newGreenDeployment.spec.template.metadata.labels;
122 | next();
123 | } catch (err) {
124 | next({
125 | log: `Encountered an error in DeploymentController.createGreenDeployment: ${err}`,
126 | status: 500,
127 | message: 'An error occured creating the green deployment',
128 | });
129 | }
130 | },
131 | createCanaryDeployment: async (req, res, next) => {
132 | const { newImage, oldYaml, targetNamespace } = req.body;
133 | try {
134 | // make a new deployment yaml changing replicas to one and adding the labels and selectors
135 | const canaryDeployment = JSON.parse(
136 | JSON.stringify({
137 | spec: { ...oldYaml.spec, replicas: 1 },
138 | metadata: {
139 | name: oldYaml.metadata.name,
140 | labels: {
141 | ...oldYaml.metadata.labels,
142 | env: 'canary',
143 | },
144 | },
145 | })
146 | );
147 | //change label
148 | canaryDeployment.spec.template.metadata.labels.env = 'canary';
149 |
150 | //edit the name if it has already been canary released
151 | const sliceIndex =
152 | canaryDeployment.metadata.name.indexOf('-canary') === -1
153 | ? canaryDeployment.metadata.name.length
154 | : canaryDeployment.metadata.name.indexOf('-canary');
155 |
156 | canaryDeployment.metadata.name =
157 | canaryDeployment.metadata.name.slice(0, sliceIndex) +
158 | '-canary' +
159 | Date.now().toString();
160 |
161 | canaryDeployment.spec.template.spec.containers[0].image = newImage;
162 | const newCanaryDeployment = (
163 | await client.apis.apps.v1
164 | .namespaces(targetNamespace)
165 | .deployments.post({ body: canaryDeployment })
166 | ).body;
167 |
168 | res.locals.canaryDeploymentName = newCanaryDeployment.metadata.name;
169 | next();
170 | } catch (err) {
171 | next({
172 | log: `Encountered an error in DeploymentController.createCanaryDeployment: ${err}`,
173 | status: 500,
174 | message: 'An error occured creating the canary deployment',
175 | });
176 | }
177 | next();
178 | },
179 | deleteDeployment: async (req, res, next) => {
180 | try {
181 | const { name, namespace } = req.query;
182 | await client.apis.apps.v1
183 | .namespaces(namespace)
184 | .deployments(name)
185 | .delete();
186 | next();
187 | } catch (err) {
188 | next({
189 | log: `Encountered an error in DeploymentController.deleteDeployment: ${err}`,
190 | status: 500,
191 | message: 'An error occured deleting the deployment',
192 | });
193 | }
194 | },
195 | };
196 |
--------------------------------------------------------------------------------
/server/controllers/NamespaceController.js:
--------------------------------------------------------------------------------
1 | const client = require('../kubernetes-config');
2 |
3 | module.exports = {
4 | getNamespaces: async (req, res, next) => {
5 | try {
6 | res.locals.namespaces = (
7 | await client.api.v1.namespaces.get()
8 | ).body.items.map((namespace) => namespace.metadata.name);
9 | next();
10 | } catch (err) {
11 | next({
12 | log: `Encountered an error in NameSpaceController.getNamespaces: ${err}`,
13 | status: 400,
14 | message: 'An error occured fetching namespace',
15 | });
16 | }
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/server/controllers/NodeController.js:
--------------------------------------------------------------------------------
1 | const client = require('../kubernetes-config');
2 |
3 | module.exports = {
4 | getNodes: async (req, res, next) => {
5 | try {
6 | res.locals.nodes = (await client.api.v1.nodes.get()).body.items;
7 | next();
8 | } catch (err) {
9 | next({
10 | log: `Encountered an error in NodeController.getNodes: ${err}`,
11 | status: 400,
12 | message: 'An error occured fetching nodes',
13 | });
14 | }
15 | },
16 | updateNode: async (req, res, next) => {
17 | try {
18 | await client.api.v1.nodes(req.query.name).put({ body: req.body });
19 | next();
20 | } catch (err) {
21 | next({
22 | log: `Encountered an error in NodeController.update: ${err}`,
23 | status: 500,
24 | message: 'An error occured updating the node',
25 | });
26 | }
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/server/controllers/PodController.js:
--------------------------------------------------------------------------------
1 | const client = require('../kubernetes-config');
2 |
3 | module.exports = {
4 | getPods: async (req, res, next) => {
5 | try {
6 | res.locals.pods = (await client.api.v1.pods.get()).body.items;
7 | next();
8 | } catch (err) {
9 | next({
10 | log: `Encountered an error in PodController.getPods: ${err}`,
11 | status: 400,
12 | message: 'An error occured fetching pods',
13 | });
14 | }
15 | },
16 | updatePod: async (req, res, next) => {
17 | const namespace = req.body.namespace || 'default';
18 | try {
19 | await client.api.v1
20 | .namespaces(namespace)
21 | .pods(req.query.name)
22 | .put({ body: req.body });
23 | next();
24 | } catch (err) {
25 | next({
26 | log: `Encountered an error in podController.update: ${err}`,
27 | status: 500,
28 | message: 'An error occured updating the pod',
29 | });
30 | }
31 | },
32 |
33 | deletePod: async (req, res, next) => {
34 | try {
35 | const { name } = req.query;
36 | const namespace = req.query.namespace || 'default';
37 | await client.api.v1.namespaces(namespace).pods(name).delete();
38 | next();
39 | } catch (err) {
40 | next({
41 | log: `Encountered an error in podController.delete: ${err}`,
42 | status: 500,
43 | message: 'An error occured deleting the pod',
44 | });
45 | }
46 | },
47 | };
48 |
--------------------------------------------------------------------------------
/server/controllers/ServiceController.js:
--------------------------------------------------------------------------------
1 | const client = require('../kubernetes-config');
2 |
3 | module.exports = {
4 | getServices: async (req, res, next) => {
5 | const { namespace } = req.query;
6 | if (namespace) {
7 | try {
8 | res.locals.services = (
9 | await client.api.v1.namespaces('default').services.get()
10 | ).body.items;
11 | return next();
12 | } catch (err) {
13 | next({
14 | log: `Encountered an error in ServiceController.get: ${err}`,
15 | status: 400,
16 | message: 'An error occured fetching services',
17 | });
18 | }
19 | } else {
20 | const { config, patch } = req.body;
21 | const { name } = req.query;
22 | try {
23 | res.locals.services = (await client.api.v1.services.get()).body.items;
24 | next();
25 | } catch (err) {
26 | next({
27 | log: `Encountered an error in ServiceController.get: ${err}`,
28 | status: 400,
29 | message: 'An error occured fetching services',
30 | });
31 | }
32 | }
33 | },
34 | updateService: async (req, res, next) => {
35 | const namespace = req.body.namespace || 'default';
36 | const { name } = req.query;
37 | const { config, patch } = req.body;
38 | if (patch) {
39 | try {
40 | await client.api.v1
41 | .namespaces(namespace)
42 | .services(name)
43 | .patch({
44 | body: { spec: { selector: config }, metadata: { labels: config } },
45 | });
46 | next();
47 | } catch (err) {
48 | next({
49 | log: `Encountered an error in ServiceController.update: ${err}`,
50 | status: 500,
51 | message: 'An error occured updating the service',
52 | });
53 | }
54 | } else {
55 | try {
56 | await client.api.v1
57 | .namespaces(namespace)
58 | .services(name)
59 | .put({ body: config });
60 | next();
61 | } catch (err) {
62 | next({
63 | log: `Encountered an error in ServiceController.update: ${err}`,
64 | status: 500,
65 | message: 'An error occured updating the service',
66 | });
67 | }
68 | }
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/server/kubernetes-config.js:
--------------------------------------------------------------------------------
1 | const { Client } = require('kubernetes-client');
2 |
3 | module.exports = new Client({ version: '1.13' });
4 |
--------------------------------------------------------------------------------
/server/routes/apiRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const podRouter = require('./resourceRoutes/podRouter');
3 | const deploymentRouter = require('./resourceRoutes/deploymentRouter');
4 | const nodeRouter = require('./resourceRoutes/nodeRouter');
5 | const serviceRouter = require('./resourceRoutes/serviceRouter');
6 | const namespaceRouter = require('./resourceRoutes/namespaceRouter');
7 |
8 | const apiRouter = express.Router();
9 |
10 | apiRouter.use('/pods', podRouter);
11 | apiRouter.use('/nodes', nodeRouter);
12 | apiRouter.use('/deployments', deploymentRouter);
13 | apiRouter.use('/services', serviceRouter);
14 | apiRouter.use('/namespaces', namespaceRouter);
15 |
16 | module.exports = apiRouter;
17 |
--------------------------------------------------------------------------------
/server/routes/resourceRoutes/deploymentRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const DeploymentController = require('../../controllers/DeploymentController');
3 | const PodController = require('../../controllers/PodController');
4 | const deploymentRouter = express.Router();
5 |
6 | deploymentRouter.get(
7 | '/',
8 | DeploymentController.getDeployments,
9 | (req, res, next) => {
10 | return res.status(200).json(res.locals.deployments);
11 | }
12 | );
13 |
14 | deploymentRouter.put(
15 | '/scale',
16 | DeploymentController.scaleDeployment,
17 | (req, res, next) => {
18 | return res.status(200).json(res.locals.deployment);
19 | }
20 | );
21 |
22 | deploymentRouter.put(
23 | '/',
24 | DeploymentController.updateDeployment,
25 | (req, res, next) => {
26 | return res.sendStatus(200);
27 | }
28 | );
29 |
30 | deploymentRouter.post(
31 | '/bluegreen',
32 | DeploymentController.createGreenDeployment,
33 | (req, res, next) => {
34 | res.status(200).json({
35 | greenDeploymentName: res.locals.greenDeploymentName,
36 | podSelectors: res.locals.podSelector,
37 | });
38 | }
39 | );
40 |
41 | deploymentRouter.post(
42 | '/canary',
43 | DeploymentController.createCanaryDeployment,
44 | (req, res, next) => {
45 | res.status(200).json(res.locals.canaryDeploymentName);
46 | }
47 | );
48 |
49 | deploymentRouter.delete(
50 | '/',
51 | DeploymentController.deleteDeployment,
52 | (req, res, next) => {
53 | return res.sendStatus(200);
54 | }
55 | );
56 | module.exports = deploymentRouter;
57 |
--------------------------------------------------------------------------------
/server/routes/resourceRoutes/namespaceRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const NamespaceController = require('../../controllers/NamespaceController');
3 | const namespaceRouter = express.Router();
4 |
5 | namespaceRouter.get(
6 | '/',
7 | NamespaceController.getNamespaces,
8 | (req, res, next) => {
9 | return res.status(200).json(res.locals.namespaces);
10 | }
11 | );
12 |
13 | module.exports = namespaceRouter;
14 |
--------------------------------------------------------------------------------
/server/routes/resourceRoutes/nodeRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const NodeController = require('../../controllers/NodeController');
3 | const nodeRouter = express.Router();
4 |
5 | nodeRouter.get('/', NodeController.getNodes, (req, res, next) => {
6 | return res.status(200).json(res.locals.nodes);
7 | });
8 |
9 | nodeRouter.put('/', NodeController.updateNode, (req, res, next) => {
10 | return res.sendStatus(200);
11 | });
12 |
13 | module.exports = nodeRouter;
14 |
--------------------------------------------------------------------------------
/server/routes/resourceRoutes/podRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const PodController = require('../../controllers/PodController');
3 | const podRouter = express.Router();
4 |
5 | podRouter.get('/', PodController.getPods, (req, res, next) => {
6 | return res.status(200).json(res.locals.pods);
7 | });
8 |
9 | podRouter.put('/', PodController.updatePod, (req, res, next) => {
10 | return res.sendStatus(200);
11 | });
12 |
13 | podRouter.delete('/', PodController.deletePod, (req, res, next) => {
14 | return res.sendStatus(200);
15 | });
16 |
17 | module.exports = podRouter;
18 |
--------------------------------------------------------------------------------
/server/routes/resourceRoutes/serviceRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const ServiceController = require('../../controllers/ServiceController');
3 | const serviceRouter = express.Router();
4 |
5 | serviceRouter.get('/', ServiceController.getServices, (req, res, next) => {
6 | return res.status(200).json(res.locals.services);
7 | });
8 |
9 | serviceRouter.put('/', ServiceController.updateService, (req, res, next) => {
10 | return res.sendStatus(200);
11 | });
12 |
13 | module.exports = serviceRouter;
14 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const apiRouter = require('./routes/apiRouter');
4 |
5 | const app = express();
6 | const PORT = 3000;
7 |
8 | app.use(express.json());
9 | app.use(express.urlencoded({ extended: true }));
10 |
11 | app.use(express.static(path.resolve(__dirname, '../client/assets')));
12 |
13 | app.get('/', (req, res) =>
14 | res.status(200).sendFile(path.resolve(__dirname, '../index.html'))
15 | );
16 |
17 | app.use('/api', apiRouter);
18 |
19 | if (process.env.NODE_ENV === 'production') {
20 | app.use('/build', express.static(path.resolve(__dirname, '../build')));
21 | }
22 |
23 | app.get('*', (req, res) =>
24 | res.status(200).sendFile(path.join(__dirname, '../index.html'))
25 | );
26 |
27 | app.use((err, req, res, next) => {
28 | const defaultErr = {
29 | log: 'Express error handler caught unknown middleware error!',
30 | status: 400,
31 | message: { err: 'An error occurred' },
32 | };
33 | const errorObj = Object.assign({}, defaultErr, err);
34 | return res.status(errorObj.status).json(errorObj.message);
35 | });
36 |
37 | app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
38 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | devServer: {
5 | publicPath: '/build/',
6 | historyApiFallback: true,
7 | proxy: {
8 | '/api': 'http://localhost:3000',
9 | },
10 | port: 8080,
11 | hot: true,
12 | },
13 | entry: ['babel-polyfill', './client/index.js'],
14 | output: {
15 | path: path.resolve(__dirname, 'build'),
16 | filename: 'bundle.js',
17 | publicPath: '/',
18 | },
19 | mode: process.env.NODE_ENV,
20 | plugins: [],
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: ['@babel/preset-env', '@babel/preset-react'],
30 | },
31 | },
32 | },
33 | {
34 | test: /\.s?css$/,
35 | use: ['style-loader', 'css-loader', 'sass-loader'],
36 | },
37 | {
38 | test: /\.(png|jpg|gif)$/i,
39 | use: [
40 | {
41 | loader: 'url-loader',
42 | options: {
43 | limit: 8192,
44 | },
45 | },
46 | ],
47 | },
48 | ],
49 | },
50 | };
51 |
--------------------------------------------------------------------------------