├── .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 | ![homepage](https://i.imgur.com/9gYeh4T.png) 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 | ![deployment rollout](https://i.imgur.com/yn8Sojn.png) 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 | product's pelican logo 110 |
111 | 121 | 132 | 133 | 140 | 141 | AWS Region 142 | 143 | 151 | 152 | 153 | {/* } 155 | label='Remember me' 156 | /> */} 157 | 158 | 167 | 168 | 169 | 174 | Supports Local Clusters 175 | 176 | 177 | 178 | 183 | Supports Amazon EKS 184 | 185 | 186 | 187 | 188 | 189 | 190 | 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 | 39 | Delete this pod? 40 | 41 | 44 | 54 | 55 | 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 | 219 | 220 | {'Choose Deployment Method:'} 221 | 222 | 223 | 224 | {status} 225 | 226 | 227 | 233 | } 236 | label="Standard" 237 | /> 238 | } 241 | label="Blue-Green" 242 | /> 243 | } 246 | label="Canary" 247 | /> 248 | 249 | 250 | 251 | 252 | 255 | 262 | 263 | 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 | 45 | 46 | Permanently Delete Deployment? 47 | 48 | 49 | 50 | Press Delete to delete deployment 51 | 52 | 53 | 54 | 57 | 67 | 68 | 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 |
59 |

60 | {`${context[0] 61 | .toUpperCase() 62 | .concat(context.slice(1, context.length - 1))} Configuration Yaml`} 63 |

64 |
65 | 66 | 73 | 74 |
75 |
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 |
99 |

Modify Yaml Configuration Here:

100 |