├── .DS_Store ├── .gcloudignore ├── .gitignore ├── Dockerfile ├── README.md ├── app.yaml ├── build ├── assets │ ├── clusters-header.png │ ├── google-cloud-floating.png │ ├── google-cloud-logo-full.png │ ├── minikube-logo-full.png │ └── mkube-floating.png └── index.html ├── client ├── .DS_Store ├── App.jsx ├── assets │ ├── .DS_Store │ ├── cluster-details-header.png │ ├── clusters-header.png │ ├── clusters-header1.png │ ├── google-cloud-floating.png │ ├── google-cloud-logo-full.png │ ├── google_cloud_logo.png │ ├── milad-fakurian-background.jpg │ ├── minikube-logo-full.png │ ├── minikube_logo.png │ └── mkube-floating.png ├── components │ ├── CloudForm.jsx │ ├── CloudSetup.jsx │ ├── Clusters.jsx │ ├── DeploymentContext.jsx │ ├── Form.jsx │ ├── LandingPage.jsx │ ├── MinikubeSetup.jsx │ ├── Project.jsx │ └── YamlGenerator.jsx ├── index.html ├── index.js ├── pages │ ├── DeploymentPage.jsx │ ├── FormPage.jsx │ └── HomePage.jsx └── styles.css ├── deployment-template.yaml ├── deployment.yaml ├── package-lock.json ├── package.json ├── server ├── controllers │ ├── controller.js │ ├── googleController.js │ └── statusController.js ├── express.js └── routers │ ├── googleRouter.js │ ├── router.js │ └── statusRouter.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/.DS_Store -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | RUN npm run build 7 | RUN curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz 8 | RUN mkdir -p /usr/local/gcloud \ 9 | && tar -C /usr/local/gcloud -xvf /tmp/google-cloud-sdk.tar.gz \ 10 | && /usr/local/gcloud/google-cloud-sdk/install.sh 11 | ENV PATH $PATH:/usr/local/gcloud/google-cloud-sdk/bin 12 | 13 | EXPOSE 3000 14 | 15 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

InKubator - Easy Kubernetes Deployment Tool

3 | 4 | Screenshot 2023-10-30 at 4 57 24 PM 5 | 6 |

Tech Stack

7 | 8 | Screenshot 2023-10-31 at 4 38 59 PM 9 | 10 | 11 |

What is Kubernetes?

12 |

13 | In the realm of traditional infrastructure, each application typically operates on a single physical server. However, the landscape of modern application architecture is significantly more intricate. Web applications now come bundled in containers, which are self-contained packages comprising segments of an application and all the necessary dependencies. This innovative approach poses a challenge for operational teams, as they are tasked with scheduling, deploying, automating, and scaling dozens or even hundreds of containers. 14 | Kubernetes, also known as K8s, is an open-source system for automating deployment, scaling, and management of containerized applications. Kubernetes enhances reliability and minimizes the time and resources required for daily operations. Kubernetes also offers storage provisions, load balancing, autoscaling and self-healing. 15 |

16 |

17 | The fundamental elements of Kubernetes architecture are clusters. Each cluster is composed of nodes, each of which represents a single compute host (virtual or physical machine). Each cluster comprises a master node (also known as control plane) which makes global decisions about the entire cluster, along with multiple worker nodes that handle containerized applications. Here is an illustration of a simplified version of Kubernetes components.

18 | Screenshot 2023-10-30 at 2 52 31 PM 19 |

20 | However, both the master node and worker node are considerably more complex systems, each incorporating multiple components and processes that operate within them. The components of the master node include the API server, etcd, kube-scheduler, kube-controller-manager, and kube-cloud-manager. Each worker node encompasses a variety of components: kubelet, kube-proxy, container runtime. Additionally, various addons, such as container resource monitoring, network plugins, and web user interfaces, further enhance the capabilities of Kubernetes.

21 | 22 | Screenshot 2023-10-30 at 2 48 52 PM 23 |

24 | When user creates an object in Kubernetes, they must provide the object spec that describes its desired state, as well as some basic information about the object. Most often this information is provided to Kubernetes CLI in the file know as deployment manifrst. YAML is a language used to provide configuration for Kubernetes. 25 |

26 | 27 |

What is InKubator?

28 |

Understanding Kubernetes architecture can be complex, and the process of deploying a cluster is not always straightforward. Even minor syntax errors or incorrect indentation of YAML manifrst can significantly complicate the deployment process, especially for those new to Kubernetes. InKubator is a developer tool designed to simplify YAML generation and cluster deployment. It enables users to deploy clusters locally on their machines using Minikube or in the cloud with the Google Kubernetes Engine (GKE). 29 |

30 |

To Get Started

31 | InKubator requires you to have Docker installed on your machine. Please dowload and install the appropriate version for your operating system. 32 |

Minikube

33 |

34 | To test InKubator using minikube, ensure that your machine meets the following requirements: 2 CPU or more, 2GB of free memory, 20GB of free disk space, and an active internet connection. If you haven't already, please install Minikube on your machine. Alternatively, you can install the latest minikube stable release on x86-64 macOS using binary download: 35 | 36 | curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 37 | sudo install minikube-darwin-amd64 /usr/local/bin/minikube 38 | 39 |

40 |

41 | Once installed, start minikube in your terminal: 42 | minikube start 43 | and navigate to InKubator. 44 | InKubator enables you to deploy your containerized app effortlessly, requiring only your public image. Alternatively, you can utilize a sample app provided by InKubator to test Kubernetes deployment. 45 |

46 |

Cloud Deployment

47 |

Before deploying on Google Cloud, ensure that the gloud CLI is installed on your machine. Additionally, you will need to manage authentication between the client and Google Kubernetes Engine. Run gke-gcloud-auth-plugin or click here for more information. 48 |

49 |

Features

50 | Let InKubator guide you through YAML configuration process. Just fill out a straightforward form, and it will generate the YAML manifest for you. Moreover, you can conveniently preview the YAML file before applying it. If you're considering deploying to Google Cloud, InKubator offers the option to either create a new cluster or utilize an existing one. Keep in mind that setting up a new cluster may take up to 10 minutes. Additionally, you have the flexibility to expose your application to external IP requests. Lastly, InKubator provides additional information on the clusters you just deployed, including the deployment name, image, replicas, pods, and pods health. 53 |

54 | 55 |

56 | InKubator is currently offers a beta version. Our team is actively expanding InKubator, incorporating features such as multiple node deployment, visualization, and advanced monitoring capabilities. Stay tuned for the latest updates and developments! 57 |

58 |

Contributions

59 |

60 | Contributions are the cornerstone of the Open Source Community, making it an incredible space for learning, development, and innovation. InKubator, as an Open Source project, eagerly welcomes contributions. Begin by forking the dev branch and creating a feature branch in your repository. Ensure that all pull requests originate from your feature branch and are directed to InKubator's dev branch. Feel free to open an issue as well! 61 |

62 |

Publications

63 | Read our Medium article here. 64 |

About the team

65 | 66 | 67 | 70 | 73 | 76 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 91 | 93 | 95 | 96 | 97 | 99 | 101 | 104 | 106 | 107 |
68 | Tarik Bensalem 69 | 71 | Rita Bizhan 72 | 74 | Jeff Chan 75 | 77 | Christina Flores 78 |
Tarik BensalemRita BizhanJeff ChanCristina Flores
88 | LinkedIn 90 | LinkedIn 92 | LinkedIn 94 | LinkedIn
98 | GitHub 100 | GitHub 102 | 103 | GitHub 105 | GitHub
108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs18 2 | entrypoint: node server/express.js 3 | env_variables: 4 | PORT: 3000 5 | FRONTEND_PORT: 8080 6 | handlers: 7 | - url: /(.*\..+)$ 8 | static_files: build/\1 9 | upload: build/(.*\..+)$ 10 | - url: /.* 11 | static_files: build/index.html 12 | upload: build/index.html -------------------------------------------------------------------------------- /build/assets/clusters-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/build/assets/clusters-header.png -------------------------------------------------------------------------------- /build/assets/google-cloud-floating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/build/assets/google-cloud-floating.png -------------------------------------------------------------------------------- /build/assets/google-cloud-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/build/assets/google-cloud-logo-full.png -------------------------------------------------------------------------------- /build/assets/minikube-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/build/assets/minikube-logo-full.png -------------------------------------------------------------------------------- /build/assets/mkube-floating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/build/assets/mkube-floating.png -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Inkubator 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/.DS_Store -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import { DeploymentProvider } from './components/DeploymentContext.jsx'; 4 | 5 | import HomePage from './pages/HomePage'; 6 | import FormPage from './pages/FormPage'; 7 | import DeploymentPage from './pages/DeploymentPage'; 8 | 9 | 10 | const App = () => { 11 | return ( 12 | <> 13 | 14 | 15 | }/> 16 | }/> 17 | }/> 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default App; -------------------------------------------------------------------------------- /client/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/.DS_Store -------------------------------------------------------------------------------- /client/assets/cluster-details-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/cluster-details-header.png -------------------------------------------------------------------------------- /client/assets/clusters-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/clusters-header.png -------------------------------------------------------------------------------- /client/assets/clusters-header1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/clusters-header1.png -------------------------------------------------------------------------------- /client/assets/google-cloud-floating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/google-cloud-floating.png -------------------------------------------------------------------------------- /client/assets/google-cloud-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/google-cloud-logo-full.png -------------------------------------------------------------------------------- /client/assets/google_cloud_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/google_cloud_logo.png -------------------------------------------------------------------------------- /client/assets/milad-fakurian-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/milad-fakurian-background.jpg -------------------------------------------------------------------------------- /client/assets/minikube-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/minikube-logo-full.png -------------------------------------------------------------------------------- /client/assets/minikube_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/minikube_logo.png -------------------------------------------------------------------------------- /client/assets/mkube-floating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/InKubator/361cbbdbc6f3d891960cc5921bd1c6375e19d125/client/assets/mkube-floating.png -------------------------------------------------------------------------------- /client/components/CloudForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Box, Chip, Grid, Button, Stack, Fab, Typography, CircularProgress, Tooltip, Paper } from '@mui/material'; 3 | import { Link, animateScroll as scroll } from 'react-scroll'; 4 | import { InfoOutlined } from "@mui/icons-material"; 5 | import clustersHeader from '../assets/clusters-header.png' 6 | import Clusters from './Clusters' 7 | import Form from './Form'; 8 | import Project from "./Project"; 9 | 10 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 11 | const theme = createTheme({ 12 | palette: { 13 | purple: { 14 | main: '#8870E0', 15 | light: '#e2e5fa', 16 | contrastText: '#fff' 17 | }, 18 | }, 19 | }); 20 | 21 | const CloudForm = () => { 22 | const [clusters, setClusters] = useState([]); 23 | const [clusterName, setClusterName] = useState(''); 24 | const [location, setLocation] = useState(null); 25 | const [status, setStatus] = useState(null); 26 | const [getCreds, setGetCreds] = useState(false); 27 | const [isLoading, setIsLoading] = useState(true); 28 | const [projects, setProjects] = useState([]); 29 | const [projectsLoaded, setProjectsLoaded] = useState(false); 30 | const [selectedProject, setSelectedProject] = useState(); 31 | 32 | const fetchRequest = async (endpoint, method, card) => { 33 | // If no "method" is passed, it uses this default header 34 | let defaultHeader = { 35 | method: "GET", 36 | headers: { 37 | "Content-Type": "application/json" 38 | }, 39 | body: JSON.stringify(card) 40 | }; 41 | // If a method is is passed, it updates the default header 42 | let header = Object.assign({}, defaultHeader, method); 43 | 44 | const result = await fetch(`${endpoint}`, header) 45 | .then((data) => data.json()) 46 | // .then((data) => console.log('DATA', data)) 47 | .catch((err) => console.error(err)) 48 | return result; 49 | } 50 | 51 | // Get clusters from selected Google Cloud project 52 | const handleGetClusters = async (e) => { 53 | const allClusters = await (fetchRequest('/google/getClusters', {method: "POST"})); 54 | console.log(allClusters) 55 | await setClusters(allClusters) 56 | setIsLoading(prevState => !prevState) // toggle isLoading true/false 57 | } 58 | 59 | const handleGetCredentials = async (e) => { 60 | const credsAreTied = await (fetchRequest('google/getCredentials', {method: "POST"}, {"clusterName": clusterName, "location": location})) 61 | await setGetCreds(credsAreTied) 62 | } 63 | 64 | const handleSelectProject = async (projectID) => { 65 | async function selectProject() { 66 | const res = await fetch('/google/selectProject', { 67 | method: 'POST', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | body: JSON.stringify({ "projectID": projectID }) 72 | }) 73 | const data = await res.json() 74 | // console.log("RESPONSE BACK", data) 75 | // reset all things back to default when you choose a new project to work off of 76 | handleGetClusters() // call getClusters everytime AFTER you select a project 77 | setGetCreds(false) 78 | setClusterName('') 79 | setStatus(null) 80 | } 81 | selectProject() 82 | } 83 | 84 | // useEffect to get projects on initial component loading 85 | useEffect(() => { 86 | async function getProjects() { 87 | const res = await fetch('/google/getProjects') 88 | const data = await res.json() 89 | console.log("Projects", data) 90 | setProjects(data) 91 | } 92 | getProjects() 93 | }, []) 94 | 95 | // Displays the selected cluster's status 96 | const statusChip = (status) => { 97 | // If status = running, make chip green 98 | // Anything else, make chip red 99 | if (status === 'RUNNING') { 100 | return 101 | } else { 102 | return 103 | } 104 | } 105 | 106 | return ( 107 | <> 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | {projects.length > 0 ? projects.map((project) => { 118 | return 119 | }): <> 120 | 121 | } 122 | 123 | 124 | 125 | 126 | {isLoading || !clusters.length ? // If loading, render loading circle 127 | 128 | 129 | 130 | : null 131 | } 132 | 133 | {clusters.length > 0 ? : <> } 141 | 142 | 143 | 144 | 145 | 146 | 147 | {clusterName ? ( 148 | 149 | 150 | {clusterName} 151 | 152 | 153 | Status: {statusChip(status)} 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | ) : null} 164 | 165 | 166 | 167 | 168 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | {getCreds ? (
) : null} 180 | 181 | 182 | 183 | 184 | ) 185 | } 186 | 187 | export default CloudForm; 188 | 189 | 190 | 191 | 192 | // All the code that I deleted LOOOL: 193 | 194 | 195 | 196 | 197 | 198 | // Tying kubectl commands to Gcloud 199 | // useEffect(() => { 200 | // // handleGetProjects() 201 | // // handleGetClusters() 202 | // }, []) 203 | 204 | // useEffect(() => { 205 | // // console.log('projects are here baby', projects, 'SET PROJECTS LOADED', projectsLoaded) 206 | // renderProjects() 207 | // }, [projectsLoaded]) 208 | 209 | // Set loading to false once the content renders 210 | // useEffect(() => { 211 | // setTimeout(() => { 212 | // setIsLoading(false); 213 | // }, 1000); 214 | // }, clusters) 215 | 216 | // const renderedProjectsArray = []; 217 | 218 | // const renderProjects = () => { 219 | // console.log('PROJECTS RENDERED') 220 | // if (projects) { 221 | // for (let i = 0; i < projects.length; i++) { 222 | // const project = projects[i]; 223 | // console.log('project', project) 224 | // // render a project component 225 | // } 226 | // } 227 | // } 228 | 229 | // Get projects from Google Cloud, the user will select one 230 | // const handleGetProjects = async (e) => { 231 | // const allProjects = await (fetchRequest('/google/getProjects', {method: "POST"})) 232 | // await setProjects(allProjects) 233 | // } -------------------------------------------------------------------------------- /client/components/CloudSetup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Button, Chip, Grid, IconButton, Tooltip, Stack, Fab, Box } from '@mui/material'; 3 | import { ContentCopy, KeyboardArrowUp } from '@mui/icons-material'; 4 | import { Element, Link, animateScroll as scroll } from 'react-scroll'; 5 | import { Link as RouterLink } from 'react-router-dom'; 6 | import googleCloudFloating from '../assets/google-cloud-floating.png' 7 | import Clusters from './Clusters' 8 | import Form from './Form'; 9 | 10 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 11 | const theme = createTheme({ 12 | palette: { 13 | purple: { 14 | main: '#8870E0', 15 | light: '#e2e5fa', 16 | contrastText: '#fff' 17 | }, 18 | }, 19 | // shape: { 20 | // borderRadius: 30, 21 | // } 22 | }); 23 | 24 | const CloudSetup = () => { 25 | const [isCopied, setIsCopied] = useState(false); 26 | const [loggedIn, setLoggedIn] = useState(false) 27 | 28 | // Reusable fetch request function 29 | const fetchRequest = async (endpoint, method, card) => { 30 | // If no "method" is passed, it uses this default header 31 | let defaultHeader = { 32 | method: "GET", 33 | headers: { 34 | "Content-Type": "application/json" 35 | }, 36 | body: JSON.stringify(card) 37 | }; 38 | // If a method is is passed, it updates the default header 39 | let header = Object.assign({}, defaultHeader, method); 40 | 41 | const result = await fetch(`${endpoint}`, header) 42 | .then((data) => data.json()) 43 | .catch((err) => console.error(err)) 44 | 45 | return result; 46 | } 47 | 48 | // Handle clicks for getting clusters, and tying credentials to KubeCTL 49 | const handleAuth = async (e) => { 50 | await (fetchRequest('google/test', {method: "POST"})); 51 | await setLoggedIn(true); 52 | } 53 | 54 | // Handle copy to clipboard 55 | const cloudStartCode = 'gcloud components install gke-gcloud-auth-plugin'; 56 | const copyToClipboard = () => { 57 | navigator.clipboard.writeText(cloudStartCode) 58 | .then(() => { 59 | setIsCopied(true); 60 | }); 61 | setTimeout(() => { 62 | setIsCopied(false); 63 | }, 2000); 64 | }; 65 | 66 | return ( 67 | 68 | 69 | {/* top of container */} 70 | 71 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |

Kubernetes deployments with Google Cloud

92 |

Before getting started, you'll need:

93 |
    94 |
  1. Google Cloud CLI installed on your computer
  2. 95 |
  3. Kubectl authentication for your Google Cloud account
  4. 96 |
  5. A containerized application
  6. 97 |
98 |
99 | 100 | 101 |

Installing the Google Cloud CLI on your machine

102 |

Visit the Google Cloud documentation for installation instructions.

103 |
104 | 105 | 106 |

Installing the Kubectl authentication plugin

107 |

Run this command in your terminal to get started:

108 |
109 |
{cloudStartCode}
110 | 111 | 112 | 113 | 114 | 115 |
116 |

Learn more about this command here!

117 |
118 | 119 | 120 | 121 | 122 | Authenticate! 123 | 124 | 125 | 126 | 127 | 128 |

Setting up your containerized application

129 |

Have the link to your containerized application ready

130 |

We support Dockerhub, Google Container Registry, etc.

131 |

To containerize your application, you can use something like Docker

132 |
133 | 134 |
135 | 136 | 137 | 138 |

Ready to deploy?

139 | 140 | 143 | 144 |
145 |
146 | 147 |
148 |
149 | ); 150 | }; 151 | 152 | export default CloudSetup; -------------------------------------------------------------------------------- /client/components/Clusters.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Box, Chip, Grid, Button, Paper, Typography, Fab, Stack, Divider, Checkbox, FormControlLabel } from '@mui/material'; 3 | import RefreshIcon from '@mui/icons-material/Refresh'; 4 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 5 | 6 | const theme = createTheme({ 7 | palette: { 8 | purple: { 9 | main: '#8870E0', 10 | light: '#e2e5fa', 11 | contrastText: '#fff' 12 | }, 13 | }, 14 | }); 15 | 16 | const Clusters = (props) => { 17 | // All declared states and variables 18 | const [showMore, setShowMore] = useState(false), [spin, setSpin] = useState(false), fullResArr = [], partialResArr = []; 19 | 20 | // OnClick event handlers 21 | const handleShowMore = async (e) => setShowMore(!showMore); 22 | const handleSelectCluster = async (e) => props.setClusterName(e.target.id); 23 | const handleSpin = (e) => { 24 | props.handleGetClusters(); 25 | setSpin(true); 26 | setTimeout(() => {setSpin(false)}, 1000); 27 | }; 28 | 29 | useEffect(() => { 30 | 31 | console.log("CLUSTERS", props.clusters) 32 | console.log("CLUSTER LENGTH", props.clusters.length) 33 | 34 | },[]) 35 | 36 | // Buttons 37 | const showMoreButton = ; 38 | const showLessButton = ; 39 | const refreshButton = ; 40 | 41 | // If clusters are passed down via props, iterate over clusters, create a result array for all info, and another for partial info 42 | if (props.clusters) { 43 | props.clusters.forEach((cluster, idx) => { 44 | const fullArr = [], partialArr = []; 45 | let button; 46 | 47 | for (let keys in cluster) { 48 | 49 | // Format the cluster name, this is the header 50 | let formattedClusterName =
51 | {cluster[keys]} 52 |
53 | 54 | // Format the status, this is important info for the user 55 | let formattedStatus = 56 | 57 | if (keys === 'NAME') { 58 | partialArr.push(
{formattedClusterName}
) 59 | fullArr.push(
{formattedClusterName}
) 60 | button =
61 | 69 |
70 | } 71 | else if (keys === 'LOCATION') { 72 | props.setLocation(cluster[keys]) 73 | partialArr.push( 74 | 75 | {keys}: {cluster[keys]} 76 | 77 | ) 78 | fullArr.push( 79 | 80 | {keys}: {cluster[keys]} 81 | 82 | ) 83 | } 84 | else if (keys === 'STATUS') { 85 | props.setStatus(cluster[keys]) 86 | partialArr.push( 87 | 88 | {keys}: {formattedStatus} 89 | 90 | ) 91 | fullArr.push( 92 | 93 | {keys}: {formattedStatus} 94 | 95 | ) 96 | } else { 97 | fullArr.push( 98 | 99 | {keys}: {cluster[keys]} 100 | 101 | ) 102 | } 103 | } 104 | fullResArr.push( 105 |
106 | 107 | {fullArr} 108 | {button} 109 | 110 |
111 | ) 112 | partialResArr.push( 113 |
114 | 115 | {partialArr} 116 | {button} 117 | 118 |
119 | ) 120 | }) 121 | } 122 | 123 | return ( 124 | 125 | 126 | 127 | 128 | {showMore ? fullResArr : partialResArr} 129 | 130 | 131 | 132 | } justifyContent='right'> 133 | {props.clusters ? (showMore ? showLessButton : showMoreButton) : null} 134 | {props.clusters ? refreshButton : null} 135 | 136 | 137 | 138 | 139 | 140 | ) 141 | } 142 | 143 | export default Clusters; -------------------------------------------------------------------------------- /client/components/DeploymentContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react'; 2 | 3 | const DeploymentContext = createContext(); 4 | 5 | export function DeploymentProvider({ children }) { 6 | const [deploymentEnvironment, setDeploymentEnvironment] = useState(localStorage.getItem('deploymentEnvironment') || ''); 7 | 8 | useEffect(() => { 9 | localStorage.setItem('deploymentEnvironment', deploymentEnvironment); 10 | }, [deploymentEnvironment]) 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | export function useDeployment() { 20 | return useContext(DeploymentContext); 21 | } 22 | -------------------------------------------------------------------------------- /client/components/Form.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Alert, Box, Grid, Button, MenuItem, IconButton, Fab, TextField, Tooltip, Typography } from '@mui/material'; 3 | import { Link as RouterLink } from 'react-router-dom'; 4 | import { InfoOutlined } from "@mui/icons-material"; 5 | import YamlGenerator from './YamlGenerator'; 6 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 7 | 8 | const theme = createTheme({ 9 | palette: { 10 | purple: { 11 | main: '#8870E0', 12 | light: '#e2e5fa', 13 | contrastText: '#fff' 14 | }, 15 | }, 16 | }); 17 | 18 | const deploymentKinds = [ 19 | { 20 | value: 'Deployment', 21 | label: 'Deployment', 22 | }, 23 | { 24 | value: 'DaemonSet', 25 | label: 'DaemonSet', 26 | }, 27 | { 28 | value: 'StatefulSet', 29 | label: 'StatefulSet', 30 | }, 31 | ]; 32 | 33 | const Form = () => { 34 | const [formValues, setFormValues] = useState({ 35 | deploymentName: { 36 | value: "", 37 | error: false, 38 | errorMessage: "Deployment name is either blank or invalid" 39 | }, 40 | labelNames: { 41 | value: "", 42 | error: false, 43 | errorMessage: "Label name is either blank or invalid" 44 | }, 45 | dockerImage: { 46 | value: "registry.k8s.io/e2e-test-images/agnhost:2.39", 47 | error: false, 48 | errorMessage: "Docker image is invalid" 49 | }, 50 | portNumber: { 51 | value: 8080, 52 | error: false, 53 | errorMessage: "Invalid port number" 54 | }, 55 | replicas: { 56 | value: 1, 57 | error: false, 58 | errorMessage: "Invalid number of replicas" 59 | }, 60 | }) 61 | 62 | // useState for (YAML, deploy, expose) button feedback rendered at the bottom 63 | const [buttonFeedback, setButtonFeedback] = useState({ 64 | feedbackMessage: "", 65 | feedbackStatus: "not pressed" 66 | }); 67 | 68 | const [yamlPreview, setYamlPreview] = useState({ 69 | portNumber: {value: ''}, 70 | replicas: {value: ''}, 71 | dockerImage: {value: ''}, 72 | }); 73 | 74 | const handleChange = (e) => { 75 | const {name, value} = e.target; 76 | setFormValues({ 77 | ...formValues, 78 | [name]:{ 79 | ...formValues[name], value 80 | } 81 | }) 82 | 83 | setYamlPreview({ 84 | ...yamlPreview, 85 | [name]:{ 86 | ...yamlPreview[name], value 87 | } 88 | }) 89 | }; 90 | 91 | // put the state inside a use effect 92 | // execute code... to update yaml 93 | // it's updating state... take that state and give it to the yaml 94 | // dependency is state value 95 | 96 | const handlePostYaml = async (e) => { 97 | e.preventDefault(); 98 | let errorThrown = false; 99 | 100 | // Perform form validation here 101 | const yamlValidationString = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; 102 | let newFormValues = {...formValues}; 103 | 104 | // Reset all fields error status back to false 105 | for (let field in newFormValues) { 106 | newFormValues[field].error = false; 107 | }; 108 | 109 | // FORM VALIDATION checking for correct data types for each field 110 | if (!yamlValidationString.test(newFormValues.deploymentName.value) || newFormValues.deploymentName.value === '') { 111 | newFormValues.deploymentName.error = true; 112 | errorThrown = true; 113 | }; 114 | if (!yamlValidationString.test(newFormValues.labelNames.value) || newFormValues.labelNames.value === '') { 115 | newFormValues.labelNames.error = true; 116 | errorThrown = true; 117 | }; 118 | if (typeof newFormValues.dockerImage.value !== 'string') { 119 | newFormValues.portNumber.error = true; 120 | errorThrown = true; 121 | }; 122 | if (newFormValues.portNumber.value < 1 || newFormValues.portNumber.value > 65535) { 123 | newFormValues.portNumber.error = true; 124 | errorThrown = true; 125 | }; 126 | if (newFormValues.replicas.value < 1) { 127 | newFormValues.replicas.error = true; 128 | errorThrown = true; 129 | }; 130 | 131 | // set form state to be newFormValues obj => update error status for fields 132 | setFormValues(newFormValues); 133 | 134 | // console.log(typeof newFormValues.replicas.value) 135 | 136 | // don't make POST request if we have an error for any of the fields 137 | if(!errorThrown) { 138 | const yamlObj = { 139 | clusterName: newFormValues.deploymentName.value, 140 | replicas: Number(newFormValues.replicas.value), 141 | image: newFormValues.dockerImage.value, 142 | port: Number(newFormValues.portNumber.value), 143 | label: newFormValues.labelNames.value 144 | }; 145 | // console.log(yamlObj); 146 | 147 | try { 148 | const postYaml = await fetch('api/yaml', { 149 | method: "POST", 150 | mode: "cors", 151 | headers: {"Content-Type": "application/json",}, 152 | body: JSON.stringify(yamlObj) 153 | }); 154 | 155 | const jsonRes = await postYaml.json(); 156 | // console.log(postYaml.status); 157 | // console.log(jsonRes); 158 | 159 | // make a copy of previous state for button feedback 160 | const prevState = {...buttonFeedback}; 161 | 162 | // if successful YAML generation from a endpoint 163 | // set rendered feedback message => YAML file generated successfully 164 | if(postYaml.status === 200) { 165 | prevState.feedbackMessage = YAML file generated successfully! 166 | prevState.feedbackStatus = "success" 167 | } else { 168 | prevState.feedbackMessage = YAML failed to generate T.T 169 | prevState.feedbackStatus = "failure" 170 | }; 171 | // console.log(prevState) 172 | setButtonFeedback(prevState); 173 | 174 | } catch(err) { 175 | console.log(`ERROR : ${err}`); 176 | }; 177 | 178 | } else { 179 | console.log("POST request NOT made"); 180 | }; 181 | }; 182 | 183 | const handleDeploy = async () => { 184 | try { 185 | const deployYaml = await fetch('/api/deploy'); 186 | const resDeploy = await deployYaml.json(); 187 | // console.log(deployYaml.status); 188 | // console.log(resDeploy); 189 | 190 | const prevState = {...buttonFeedback}; 191 | // handle button feedback here (based on status code) 192 | // use setButtonPressed 193 | // set button + buttonFeedback string 194 | if(deployYaml.status === 200) { 195 | prevState.feedbackMessage = Deployment successful! 196 | prevState.feedbackStatus = "success" 197 | } else { 198 | prevState.feedbackMessage = Deployment failed. 199 | prevState.feedbackStatus = "failure" 200 | }; 201 | // console.log(prevState) 202 | setButtonFeedback(prevState); 203 | 204 | } catch(err) { 205 | console.log(`ERROR: ${err}`); 206 | }; 207 | }; 208 | 209 | const handleExpose = async () => { 210 | try { 211 | const exposeYaml = await fetch('/api/expose') 212 | const resExpose = await exposeYaml.json(); 213 | console.log('EXPOSURE RESULTS', resExpose); 214 | 215 | const prevState = {...buttonFeedback}; 216 | // handle button feedback here (based on status code) 217 | // use setButtonPressed 218 | // set button + buttonFeedback string 219 | if(exposeYaml.status === 200) { 220 | prevState.feedbackMessage = Cluster exposed successfully! 221 | prevState.feedbackStatus = "success" 222 | } else { 223 | prevState.feedbackMessage = Failed to expose cluster. 224 | prevState.feedbackStatus = "failure" 225 | }; 226 | // console.log(prevState) 227 | setButtonFeedback(prevState); 228 | } catch(err) { 229 | console.log(`ERROR: ${err}`); 230 | }; 231 | }; 232 | 233 | return ( 234 | 235 | 236 | 237 |
238 | Launch Kubernetes with InKubator! 239 |
240 | 241 |
242 | Deployment details 243 |
244 | 245 |
246 |

Deployment kind

247 | 253 | {deploymentKinds.map((option) => ( 254 | 255 | {option.label} 256 | 257 | ))} 258 | 259 | 260 |

Deployment name

261 | handleInputChange(e, setDeploymentName)} 272 | /> 273 | 274 |

Labels

275 | handleInputChange(e, setClusterLabel)} 285 | /> 286 |
287 | 288 |
289 | Pod details 290 |
291 | 292 |
293 |

Docker image

294 | handleInputChange(e, setDockerImage)} 305 | /> 306 | 307 |

Port number

308 | handleInputChange(e, setContainerPort, true)} 324 | /> 325 | 326 |

Number of replicas

327 | handleInputChange(e, setNumReplicas, true)} 342 | /> 343 | 344 |
345 |

349 | {buttonFeedback.feedbackMessage}

350 |
351 | 352 | {/* FOOTER */} 353 |
354 | 355 | 356 | 357 | 358 |
359 |
360 | 361 | 362 | 363 |
364 |
365 | ) 366 | } 367 | 368 | export default Form; -------------------------------------------------------------------------------- /client/components/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Button, Paper } from '@mui/material'; 3 | import { Link, animateScroll as scroll} from 'react-scroll'; 4 | import googleCloudLogo from '../assets/google-cloud-logo-full.png' 5 | import minikubeLogo from '../assets/minikube-logo-full.png'; 6 | 7 | 8 | const LandingPage = ({ handleEnvironmentChange }) => { 9 | 10 | return ( 11 |
12 | 13 | 14 |

InKubator

15 |

Deployment made simple.

16 |
17 | 18 | 19 |

Where are you deploying?

20 |
21 | 22 |
23 | 24 | {/* MINIKUBE CONTAINER */} 25 |
26 | 33 | 39 | 40 |
41 | 42 | {/* CLOUD CONTAINER */} 43 |
44 | 51 | 57 | 58 |
59 |
60 |
61 | ) 62 | }; 63 | 64 | export default LandingPage; -------------------------------------------------------------------------------- /client/components/MinikubeSetup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Chip, Grid, IconButton, Tooltip, Stack } from '@mui/material'; 3 | import { ContentCopy, InfoOutlined, KeyboardArrowUp } from '@mui/icons-material'; 4 | import { Element, Link, animateScroll as scroll } from 'react-scroll'; 5 | import { Link as RouterLink } from 'react-router-dom'; 6 | import minikubeBlock from '../assets/mkube-floating.png'; 7 | 8 | 9 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 10 | const theme = createTheme({ 11 | palette: { 12 | purple: { 13 | main: '#8870E0', 14 | light: '#e2e5fa', 15 | contrastText: '#fff' 16 | }, 17 | }, 18 | // shape: { 19 | // borderRadius: 30, 20 | // } 21 | }); 22 | 23 | 24 | const MinikubeSetup = () => { 25 | const [isCopied, setIsCopied] = useState(false); 26 | 27 | // Code for copy to clipboard functionality 28 | const minikubeStartCode = 'minikube start'; 29 | const copyToClipboard = () => { 30 | navigator.clipboard.writeText(minikubeStartCode) 31 | .then(() => { 32 | setIsCopied(true); 33 | }); 34 | setTimeout(() => { 35 | setIsCopied(false); 36 | }, 2000); 37 | }; 38 | 39 | return ( 40 | 41 | 42 | 43 | {/* Scroll to landing page button */} 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |

Kubernetes deployments with Minikube

63 | 64 |

Before getting started, you'll need:

65 |
    66 |
  1. A container or virtual machine manager
  2. 67 |
  3. Minikube installed on your machine
  4. 68 |
69 | 70 | 71 |

Set up your container

72 |

We support Docker, Hyperkit, etc. All you'll need is the name of your container.

73 |
74 | 75 | 76 |

Install Minikube

77 |

Click here for instructions on how to install.

78 |
79 | 80 | 81 |

Start Minikube

82 |

Run this command in your terminal to get started.

83 |
84 |
{minikubeStartCode}
85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 | 96 |

Ready to deploy?

97 | 98 | 101 | 102 |
103 |
104 | 105 |
106 |
107 | ) 108 | }; 109 | 110 | export default MinikubeSetup; -------------------------------------------------------------------------------- /client/components/Project.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Button, Grid, Stack, Typography } from '@mui/material'; 3 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 4 | 5 | const theme = createTheme({ 6 | palette: { 7 | purple: { 8 | main: '#8870E0', 9 | light: '#e2e5fa', 10 | contrastText: '#fff' 11 | }, 12 | }, 13 | }); 14 | 15 | const Project = ({ projectData, setSelectedProject }) => { 16 | // console.log('FUNCTION', props.setSelectedProject) 17 | // console.log('DECONSTRUCTED', PROJECT_ID, NAME, PROJECT_NUMBER) 18 | // console.log('projectData IN PROJECT/JSX', projectData.projectData) 19 | // console.log("Inside of individual project", projectData); 20 | 21 | const {PROJECT_ID, NAME, PROJECT_NUMBER} = projectData; 22 | 23 | const handleSelectProject = async () => { 24 | // console.log('e.target', e.target.id) 25 | // console.log(NAME, "SELECTED INSIDE OF PROJECT COMPONENT") 26 | setSelectedProject(PROJECT_ID); 27 | }; 28 | 29 | return ( 30 | 31 | 32 |
{NAME}
33 | 34 | Project ID: {PROJECT_ID} 35 | Project Number: {PROJECT_NUMBER} 36 | 37 | 38 |
39 |
40 | ) 41 | } 42 | 43 | export default Project; -------------------------------------------------------------------------------- /client/components/YamlGenerator.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SyntaxHighlighter from 'react-syntax-highlighter'; 3 | import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 4 | import { anOldHope } from "react-syntax-highlighter/dist/esm/styles/hljs"; 5 | 6 | 7 | const YamlGenerator = ({ formValues, setFormValues, yamlPreview, setYamlPreview }) => { 8 | 9 | const codeString = ` 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: ${formValues.deploymentName.value} 14 | labels: 15 | app: ${formValues.labelNames.value} 16 | spec: 17 | selector: 18 | matchLabels: 19 | app: ${formValues.labelNames.value} 20 | template: 21 | metadata: 22 | labels: 23 | app: ${formValues.labelNames.value} 24 | spec: 25 | containers: 26 | - ports: 27 | - containerPort: ${yamlPreview.portNumber.value} 28 | name: 29 | image: ${yamlPreview.dockerImage.value} 30 | replicas: ${yamlPreview.replicas.value} 31 | `; 32 | 33 | return ( 34 | 35 | {codeString} 36 | 37 | ); 38 | }; 39 | 40 | export default YamlGenerator; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Inkubator 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import ReactDOM from 'react-dom'; // Correct the import statement 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App'; 6 | import './styles.css'; // If the styles need to be imported, use the correct path 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container); 10 | root.render() -------------------------------------------------------------------------------- /client/pages/DeploymentPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Box, Grid, Button, Stack, Paper, Typography, Link, Breadcrumbs } from '@mui/material'; 3 | import { ThemeProvider, createTheme, keyframes } from '@mui/material/styles'; 4 | import clusterDetailsHeader from '../assets/cluster-details-header.png' 5 | 6 | 7 | const theme = createTheme({ 8 | palette: { 9 | purple: { 10 | main: '#8870E0', 11 | light: '#e2e5fa', 12 | contrastText: '#fff' 13 | }, 14 | }, 15 | }); 16 | 17 | 18 | const DeploymentPage = () => { 19 | const [deploymentStats, setDeploymentStats] = useState({}); 20 | 21 | //Default deployment object 22 | let deplObjConstruct = { 23 | deployment: { 24 | name: '', 25 | pods: '', 26 | image: '', 27 | }, 28 | replicas: { 29 | name: '', 30 | pods: '', 31 | }, 32 | pods: [], 33 | }; 34 | 35 | // Helper function that converts output string to object for DEPLOYMENT 36 | const helperDeploymentObject = (str) => { 37 | // Convert to array and remove spaces and empty strings 38 | let strToArr = str.split(/(\s+)/).filter((el) => !el.includes(' ') && el !== ''); 39 | // Remove auto-generated fields 40 | strToArr = strToArr.slice(strToArr.indexOf('\n') + 1); 41 | console.log('array is ', strToArr); 42 | 43 | // Assuming this function receives only one deployment at a time 44 | if (strToArr.length > 2) { 45 | deplObjConstruct.deployment.name = strToArr[0]; 46 | deplObjConstruct.deployment.pods = strToArr[1]; 47 | deplObjConstruct.deployment.image = strToArr[6]; 48 | deplObjConstruct.replicas.pods = strToArr[1]; 49 | } else { 50 | deplObjConstruct.deployment.name = null; 51 | deplObjConstruct.deployment.pods = null; 52 | deplObjConstruct.deployment.image = null; 53 | deplObjConstruct.replicas.pods = null; 54 | } 55 | }; 56 | 57 | // Helper function that converts output string to object for PODS 58 | const helperPodsObject = (str) => { 59 | // Convert to array, remove spaces and empty strings, remove non-applicable fields 60 | let strToArr = str.split(/(\s+)/).filter((el) => !el.includes(' ') && el !== ''); 61 | strToArr = strToArr.slice(strToArr.indexOf('\n') + 1); 62 | 63 | // Get replica name 64 | let replicaName = strToArr[0].split('-').slice(0, 2).join('-'); 65 | deplObjConstruct.replicas.name = replicaName; 66 | 67 | // Iterate to store pod information in pods array of objects 68 | while (strToArr.length > 1) { 69 | let pod = { 70 | name : strToArr[0], 71 | ready : strToArr[1], 72 | status : strToArr[2], 73 | restarts : strToArr[3], 74 | }; 75 | deplObjConstruct.pods.push(pod); 76 | strToArr = strToArr.slice(strToArr.indexOf('\n')+ 1); 77 | }; 78 | }; 79 | 80 | const getStats = async () => { 81 | const fetchDeployment = await fetch('/status/getDeployment'); 82 | const deploymentInfo = await fetchDeployment.json(); 83 | helperDeploymentObject(deploymentInfo); 84 | 85 | const fetchPods = await fetch('/status/getPods'); 86 | const podsInfo = await fetchPods.json(); 87 | helperPodsObject(podsInfo); 88 | 89 | setDeploymentStats(deplObjConstruct); 90 | }; 91 | 92 | useEffect(() => { 93 | getStats(); 94 | }, []) 95 | 96 | const handleDelete = async () => { 97 | try { 98 | const deleteReq = await fetch('/status/deleteDeployment'); 99 | const deleteRes = await deleteReq.json(); 100 | console.log('Delete status ', deleteReq.status); 101 | 102 | } catch(err) { 103 | console.log(`ERROR: ${err}`); 104 | }; 105 | getStats(); 106 | } 107 | 108 | console.log('DEPLOYMENT STATS: ', deploymentStats); 109 | 110 | let deplInfoRender = []; 111 | for (let keyDepl in deploymentStats.deployment) { 112 | if (keyDepl !== 'pods') { 113 | deplInfoRender.push({keyDepl.toUpperCase()}: {deploymentStats.deployment[keyDepl]}); 114 | } 115 | }; 116 | 117 | let replicaInfoRender = []; 118 | for (let keyRepl in deploymentStats.replicas) { 119 | if (keyRepl === 'pods') { 120 | replicaInfoRender.push(
{deploymentStats.replicas.pods}
) 121 | } 122 | }; 123 | 124 | let podsInfoRender = deploymentStats.pods && Array.isArray(deploymentStats.pods) 125 | ? deploymentStats.pods.map((pod, index) => ( 126 | 127 | 128 | Pod {index + 1} 129 | 130 | 131 | {Object.keys(pod).map((key, innerIndex) => ( 132 | 133 | 134 | {key.toUpperCase()}: {pod[key]} 135 | 136 | 137 | ))} 138 | 139 | 140 | )) 141 | : null; 142 | 143 | //Get external endpoint to access app 144 | const handleGetEndPoint = async () => { 145 | try { 146 | const reqEndPoint = await fetch('/google/getEndpoint'); 147 | const endPoint = await reqEndPoint.json(); 148 | console.log(endPoint); 149 | window.open(`http://${endPoint}`) 150 | } catch(err) { 151 | console.log(`ERROR at getEndPoint: ${err}`); 152 | }; 153 | }; 154 | 155 | return ( 156 | <> 157 | 158 | 159 | Landing 160 | 161 | 162 | Form 163 | 164 | Deployment Page 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 |
179 | DEPLOYMENT 180 |
181 |
182 | {deplInfoRender} 183 |
184 |
185 |
186 |
187 | 188 | 189 | 190 | REPLICAS 191 | {replicaInfoRender} 192 | 193 | 194 | 195 | 196 | {podsInfoRender} 197 | 198 |
199 | 200 | 201 | 202 | 203 | 204 | 205 |
206 |
207 | 208 | ) 209 | }; 210 | 211 | export default DeploymentPage; -------------------------------------------------------------------------------- /client/pages/FormPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDeployment } from '../components/DeploymentContext.jsx' 3 | import { Box, Breadcrumbs, Stack, Grid, Link, Typography } from "@mui/material"; 4 | import Form from '../components/Form'; 5 | import CloudForm from '../components/CloudForm'; 6 | import YamlGenerator from "../components/YamlGenerator"; 7 | 8 | const FormPage = () => { 9 | const { deploymentEnvironment } = useDeployment(); 10 | 11 | return ( 12 | <> 13 | 14 | 15 | Landing 16 | Form 17 | 18 | {deploymentEnvironment === 'minikube' ? : } 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default FormPage; -------------------------------------------------------------------------------- /client/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDeployment } from '../components/DeploymentContext.jsx' 3 | 4 | import LandingPage from '../components/LandingPage'; 5 | import CloudSetup from "../components/CloudSetup"; 6 | import MinikubeSetup from "../components/MinikubeSetup"; 7 | 8 | const HomePage = () => { 9 | const { deploymentEnvironment, setDeploymentEnvironment } = useDeployment(); 10 | 11 | const handleEnvironmentChange = (environment) => { 12 | setDeploymentEnvironment(environment); 13 | }; 14 | 15 | return ( 16 | <> 17 |
18 | 19 |
20 | 21 |
22 | {deploymentEnvironment === '' ? null : (deploymentEnvironment === 'cloud' ? : )} 23 |
24 | 25 | ) 26 | } 27 | 28 | export default HomePage; -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); 2 | 3 | /* GLOBAL FORMATTING */ 4 | 5 | body { 6 | font-family: 'Roboto', sans-serif; 7 | background-color: #e7f0fc; 8 | color: #30292F; 9 | } 10 | 11 | 12 | 13 | /* FORM */ 14 | .form-main-container { 15 | justify-content: center; 16 | /* align-items: center; */ 17 | } 18 | 19 | .formPage{ 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .form-questions { 25 | display: flex; 26 | /* padding: 40px; */ 27 | flex-direction: column; 28 | } 29 | 30 | /* CLUSTERS */ 31 | #clusters-main-container { 32 | display: flex; 33 | align-self: center; 34 | align-items: center; 35 | justify-content: center; 36 | padding: 1%; 37 | width: 95vw; 38 | /* background-color: #64B5F6; */ 39 | } 40 | 41 | #selected-cluster-container { 42 | background-color: #e2e5fa; 43 | border-radius: 30px; 44 | margin: 1%; 45 | padding: 2%; 46 | } 47 | 48 | .cluster-paper { 49 | margin: 1%; 50 | padding: 1%; 51 | border-radius: 30px; 52 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 5px; 53 | background-color: white; 54 | /* background-color: pink; */ 55 | } 56 | 57 | .cluster-labels-container { 58 | display: flex; 59 | flex-direction: column; 60 | justify-content: center; 61 | align-content: center; 62 | padding: 1%; 63 | margin: 1%; 64 | width: 100%; 65 | /* background-color: red; */ 66 | } 67 | 68 | .cluster-labels { 69 | text-align: center; 70 | align-self: center; 71 | font-weight: 600; 72 | font-size: large; 73 | border-radius: 30px; 74 | box-sizing: border-box; ; 75 | background-color: #d0c4f7; 76 | padding: 3%; 77 | margin: 2%; 78 | border-radius: 30px; 79 | width: 100%; 80 | } 81 | 82 | .replicas-labels { 83 | text-align: center; 84 | background-color: #d0c4f7; 85 | font-weight: 600; 86 | font-size: large; 87 | border-radius: 30px; 88 | padding: 3%; 89 | margin: 3%; 90 | width: 55%; 91 | } 92 | 93 | .cluster-content { 94 | font-weight: 400; 95 | } 96 | 97 | #deployment-button-container { 98 | display: flex; 99 | flex-direction: column; 100 | width: fit-content; 101 | gap: 10px; 102 | } 103 | 104 | #deployment-button { 105 | width: 100px; 106 | max-width: 50%; 107 | justify-content: right; 108 | align-items: right; 109 | } 110 | 111 | 112 | #clusters-header { 113 | width: 95vw; 114 | height: 30vh; 115 | margin: 1%; 116 | overflow: hidden; 117 | border-radius: 30px; 118 | } 119 | 120 | #clusters-header-img { 121 | height: 100%; 122 | width: 100%; 123 | border-radius: 30px; 124 | object-fit: cover; 125 | } 126 | 127 | .clusters-container-A { 128 | background-color: #e2e5fa; 129 | border-radius: 25px; 130 | /* margin: 1%; */ 131 | /* margin: 50px; */ 132 | padding: 1%; 133 | justify-content: center; 134 | align-items: center; 135 | box-sizing: border-box; 136 | } 137 | 138 | #clusters-container-B { 139 | padding: 1%; 140 | box-sizing: border-box; 141 | } 142 | 143 | #user-clusters { 144 | display: flex; 145 | flex-direction: row; 146 | } 147 | 148 | #cluster-main-buttons { 149 | padding: 1%; 150 | align-items: right; 151 | justify-content: right; 152 | } 153 | 154 | .cluster-select-buttons { 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | } 159 | 160 | /* GOOGLE CLOUD PROJECTS */ 161 | #projects-main-container { 162 | display: flex; 163 | align-self: center; 164 | align-items: left; 165 | flex-wrap: wrap; 166 | justify-content: left; 167 | flex-wrap: wrap; 168 | margin: 1%; 169 | padding: 1%; 170 | width: 95vw; 171 | background-color: #e2e5fa; 172 | border-radius: 25px; 173 | box-sizing: border-box; 174 | } 175 | 176 | .project-cards { 177 | display: flex; 178 | flex-direction: column; 179 | padding: 1%; 180 | margin: 1%; 181 | border-radius: 30px; 182 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 5px; 183 | background-color: white; 184 | } 185 | 186 | .project-content { 187 | padding: 2%; 188 | } 189 | 190 | 191 | /*DEVELOPMENT STATS */ 192 | #deployment-page-main-container { 193 | display: flex; 194 | flex-direction: column; 195 | align-items: center; 196 | justify-content: center; 197 | padding: 1%; 198 | width: 98vw; 199 | } 200 | 201 | #development-main-container { 202 | display: flex; 203 | justify-content: center; 204 | align-items: center; 205 | background-color: #e2e5fa; 206 | border-radius: 30px; 207 | padding: 1%; 208 | margin: 1%; 209 | box-sizing: border-box; 210 | } 211 | 212 | .development-container-class { 213 | background-color: white; 214 | border-radius: 25px; 215 | padding: 2%; 216 | box-sizing: border-box; 217 | } 218 | 219 | #deployment-box, #replica-box { 220 | height: 20vh; 221 | display: flex; 222 | flex-direction: column; 223 | justify-content: center; 224 | align-items: center; 225 | } 226 | 227 | .development-stat-header { 228 | display: flex; 229 | justify-content: center; 230 | align-content: center; 231 | padding: 2%; 232 | width: 100%; 233 | background-color: green ; 234 | box-sizing: border-box; 235 | } 236 | 237 | .development-stat-header-label { 238 | text-align: center; 239 | font-weight: 600; 240 | font-size: large; 241 | padding: 6%; 242 | border-radius: 30px; 243 | background-color: #d3d7f1; 244 | background-color: aqua; 245 | } 246 | 247 | #total-pods-formatted { 248 | display: flex; 249 | justify-content: center; 250 | align-items: center; 251 | text-align: center; 252 | font-weight: 600; 253 | font-size: xx-large; 254 | padding: 2%; 255 | margin: 2%; 256 | } 257 | 258 | #deployment-parent-container { 259 | justify-content: center; 260 | align-items: center; 261 | } 262 | 263 | #pods-info-container { 264 | display: flex; 265 | flex-direction: column; 266 | justify-content: center; 267 | align-items: center; 268 | padding: 5%; 269 | margin: 1%; 270 | } 271 | 272 | #replicas-count-box { 273 | height: 100%; 274 | width: 100%; 275 | } 276 | 277 | .pod-name { 278 | text-align: center; 279 | font-weight: 600; 280 | font-size: large; 281 | padding: 1%; 282 | border-radius: 30px; 283 | background-color: #e2e5fa; 284 | } 285 | 286 | #deployment-detail-header { 287 | width: 100%; 288 | height: 30vh; 289 | margin: 1%; 290 | overflow: hidden; 291 | border-radius: 30px; 292 | } 293 | 294 | #deployment-info-div { 295 | /* background-color: yellowgreen; */ 296 | } 297 | 298 | #cluster-detail-header-img { 299 | height: 100%; 300 | width: 100%; 301 | border-radius: 30px; 302 | object-fit: cover; 303 | } 304 | 305 | @keyframes spin { 306 | from { 307 | transform: rotate(0deg); 308 | } 309 | to { 310 | transform: rotate(360deg); 311 | } 312 | } 313 | 314 | .spin { 315 | animation: spin 1s linear infinite; 316 | } 317 | 318 | 319 | 320 | #form { 321 | /* CSS GRID */ 322 | display: grid; 323 | grid-template-columns: 0.5fr 1.5fr; 324 | grid-template-rows: 0.1fr 0.8fr 0.8fr 0.1fr; 325 | grid-template-areas: 326 | "form-header form-header ." 327 | "form-div1 form-div2 ." 328 | "form-div3 form-div4 ." 329 | "form-footer form-footer ."; 330 | /* LAYOUT */ 331 | padding: 20px; 332 | margin-top: 20px; 333 | min-width: 500px; 334 | max-width: 700px; 335 | border-radius: 30px; 336 | /* PRETTY COLOR THINGS */ 337 | background-color: white; 338 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 339 | } 340 | 341 | .form-header { 342 | grid-area: form-header; 343 | height: auto; 344 | padding: 1%; 345 | margin: 1%; 346 | font-weight: 600; 347 | font-size: xx-large; 348 | justify-self: center; 349 | } 350 | 351 | .form-section-header { 352 | padding: 20px; 353 | } 354 | 355 | /* FORM SECTION HEADERS BY ID */ 356 | #form-div1 { grid-area: form-div1; margin-top: 8px;} 357 | #form-div3 { grid-area: form-div3; margin-top: 8px;} 358 | 359 | /* FORM QUESTION SECTIONS BY ID */ 360 | .form-div2 { grid-area: form-div2; margin-right: 30px; padding: 10px;} 361 | .form-div4 { grid-area: form-div4; margin-right: 30px; padding: 10px;} 362 | 363 | .form-footer { 364 | grid-area: form-footer; 365 | display: flex; 366 | flex-direction: row; 367 | gap: 10px; 368 | margin-top: 20px; 369 | justify-self: center; 370 | } 371 | 372 | #minikube-form { 373 | display: grid; 374 | grid-template-columns: 0.7fr 1.3fr; 375 | grid-template-rows: 1fr 1fr; 376 | gap: 0px 0px; 377 | grid-template-areas: 378 | "form-header1 form-questions1" 379 | "form-header2 form-questions2"; 380 | min-width: 700px; 381 | max-width: 900px; 382 | padding: 30px; 383 | background-color: white; 384 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 385 | } 386 | 387 | #the-code-block { 388 | margin-top: 20px; 389 | border-radius: 5px; 390 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 391 | } 392 | 393 | /* LANDING PAGE */ 394 | #homepage-container { 395 | display: flex; 396 | flex-direction: column; 397 | align-items: center; 398 | justify-content: center; 399 | justify-self: center; 400 | align-self: center; 401 | } 402 | 403 | .landing { 404 | display: flex; 405 | flex-direction: column; 406 | justify-content: center; 407 | align-items: center; 408 | text-align: center; 409 | margin: 10px; 410 | height: 92vh; 411 | width: 95vw; 412 | background-image: url(https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?auto=format&fit=crop&q=80&w=2874&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D); 413 | background-size: cover; 414 | border-radius: 30px; 415 | } 416 | 417 | #landing-header-container { 418 | margin-bottom: 40px; 419 | } 420 | 421 | #landing-header-title { 422 | font-weight: 500; 423 | font-size: 80px; 424 | margin: 0; 425 | padding: 10px; 426 | } 427 | 428 | #landing-header-text { 429 | font-weight: 400; 430 | font-size: 33px; 431 | margin: 0; 432 | padding: 0; 433 | } 434 | 435 | #landing-page-button-container { 436 | display: flex; 437 | flex-direction: row; 438 | justify-content: center; 439 | align-items: center; 440 | gap: 50px; 441 | } 442 | 443 | #landing-body-text { 444 | font-size: 25px; 445 | font-weight: 400; 446 | } 447 | 448 | .landing-page-buttons { 449 | display: flex; 450 | flex-direction: column; 451 | justify-content: center; 452 | align-items: center; 453 | } 454 | 455 | .landing-page-button { 456 | background-color: #e2e5fa; 457 | border: 0; 458 | border-radius: 30px; 459 | width: 420px; 460 | height: 20vh; 461 | box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, 0.25); 462 | transition: all .2s ease-in-out; 463 | } 464 | 465 | .landing-page-button:hover { 466 | transform: scale(1.1); 467 | } 468 | 469 | /* SETUP PAGE */ 470 | #setup-container { 471 | display: flex; 472 | flex-direction: column; 473 | justify-content: center; 474 | align-items: center; 475 | text-align: left; 476 | } 477 | 478 | #cloud-setup-instructions { 479 | width: 95vw; 480 | margin: 2%; 481 | padding: 1%; 482 | } 483 | 484 | #minikube-setup-instructions { 485 | width: 95vw; 486 | margin: 2%; 487 | padding: 1%; 488 | } 489 | 490 | .setup-img-container { 491 | padding: 2%; 492 | } 493 | 494 | .setup-img { 495 | height: 100%; 496 | width: 100%; 497 | object-fit: cover; 498 | border-radius: 30px; 499 | } 500 | 501 | .setup-header { 502 | display: flex; 503 | align-items: center; 504 | justify-content: center; 505 | } 506 | 507 | .setup-requirements { 508 | padding: 2%; 509 | } 510 | 511 | .setup-paper { 512 | background-color: rgb(253, 249, 249); 513 | border-radius: 30px; 514 | padding: 1px 5px 5px 5px; 515 | margin: 5px; 516 | } 517 | 518 | .setup-content { 519 | padding: 1%; 520 | } 521 | 522 | .setup-footer { 523 | display: flex; 524 | flex-direction: column; 525 | align-items: center; 526 | justify-content: center; 527 | /* background-color: #64B5F6; */ 528 | } 529 | 530 | .code-snippet { 531 | display: flex; 532 | flex-direction: row; 533 | color: #272a36; 534 | /* background-color: #272a36; */ 535 | background-color: #d3d7f1; 536 | border: solid #ededed 1px; 537 | border-radius: 30px; 538 | padding-left: 30px; 539 | padding-right: 20px; 540 | margin-left: 10px; 541 | margin-right: 10px; 542 | width: auto; 543 | justify-content: space-between; 544 | } 545 | 546 | #setup-form-continue-btn-container { 547 | display: flex; 548 | justify-content: center; 549 | align-items: center; 550 | } 551 | 552 | #ready-to-deploy { 553 | font-weight: 700; 554 | font-size: xx-large; 555 | } 556 | 557 | /* FORMATTING FOR NAVIGATION */ 558 | .nav-button { 559 | display: flex; 560 | align-content: center; 561 | justify-content: center; 562 | } 563 | 564 | .section { 565 | transition: transform 0.5s ease-in-out; 566 | transform: translateY(100%); 567 | } 568 | 569 | .section.cloud { 570 | background-color: #E57373; 571 | } 572 | 573 | .section.setup { 574 | background-color: greenyellow; 575 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 576 | } 577 | 578 | .section.form { 579 | background-color: #64B5F6; 580 | } 581 | 582 | .section.active { 583 | transform: translateY(0); 584 | } 585 | 586 | -------------------------------------------------------------------------------- /deployment-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: 5 | labels: 6 | app: 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: 11 | template: 12 | metadata: 13 | labels: 14 | app: 15 | spec: 16 | containers: 17 | - ports: 18 | - containerPort: 3000 19 | name: 20 | image: 21 | replicas: 3 22 | -------------------------------------------------------------------------------- /deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ttt 5 | labels: 6 | app: lll 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: lll 11 | template: 12 | metadata: 13 | labels: 14 | app: lll 15 | spec: 16 | containers: 17 | - ports: 18 | - containerPort: 3000 19 | name: lll 20 | image: margaritabizhan/getting-started 21 | replicas: 3 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "k8", 3 | "version": "1.0.0", 4 | "description": "inKubator", 5 | "main": "server/express.js", 6 | "scripts": { 7 | "start": "node server/express.js", 8 | "dev": "webpack-dev-server --open & nodemon server/express.js", 9 | "start fake": "webpack-dev-server --open & nodemon server/express.js", 10 | "build": "webpack" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@emotion/react": "^11.11.1", 17 | "@emotion/styled": "^11.11.0", 18 | "@fontsource/roboto": "^5.0.8", 19 | "@mui/icons-material": "^5.14.14", 20 | "@mui/material": "^5.14.14", 21 | "child_process": "^1.0.2", 22 | "cors": "^2.8.5", 23 | "express": "^4.18.2", 24 | "js-yaml": "^4.1.0", 25 | "nodemon": "^3.0.1", 26 | "path": "^0.12.7", 27 | "react-router-dom": "^6.17.0", 28 | "react-scroll": "^1.9.0", 29 | "react-syntax-highlighter": "^15.5.0", 30 | "yaml-loader": "^0.8.0", 31 | "npm": "^10.2.0", 32 | "react": "^18.2.0", 33 | "react-dom": "^18.2.0", 34 | "webpack": "^5.89.0", 35 | "webpack-cli": "^5.1.4" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.23.2", 39 | "@babel/preset-env": "^7.23.2", 40 | "@babel/preset-react": "^7.22.15", 41 | "@emotion/react": "^11.11.1", 42 | "@emotion/styled": "^11.11.0", 43 | "@fontsource/roboto": "^5.0.8", 44 | "@mui/icons-material": "^5.14.14", 45 | "@mui/material": "^5.14.14", 46 | "babel-loader": "^9.1.3", 47 | "concurrently": "^8.2.1", 48 | "css-loader": "^6.8.1", 49 | "eslint": "^8.51.0", 50 | "file-loader": "^6.2.0", 51 | "html-webpack-plugin": "^5.5.3", 52 | "install": "^0.13.0", 53 | "nodemon": "^3.0.1", 54 | "npm": "^10.2.0", 55 | "react": "^18.2.0", 56 | "react-dom": "^18.2.0", 57 | "sass": "^1.69.3", 58 | "style-loader": "^3.3.3", 59 | "url-loader": "^4.1.1", 60 | "webpack": "^5.89.0", 61 | "webpack-cli": "^5.1.4", 62 | "webpack-dev-server": "^4.15.1", 63 | "webpack-hot-middleware": "^2.25.4", 64 | "yaml-loader": "^0.8.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/controllers/controller.js: -------------------------------------------------------------------------------- 1 | const { exec, spawn, ChildProcess } = require('child_process'); 2 | const yaml = require('js-yaml'); 3 | const fs = require('fs'); 4 | 5 | let tunnelProcess; 6 | let minikubeProcess; 7 | 8 | const controller = {}; 9 | 10 | controller.startMinikube = function(req, res, next) { 11 | if(!minikubeProcess) { 12 | minikubeProcess = spawn('minikube', ['start']); 13 | minikubeProcess.stdout.on('data', (data) => { 14 | console.log(`Minikube start initiated. Message from minikube ${data}`); 15 | return next(); 16 | }); 17 | minikubeProcess.on('error', (error) => { 18 | console.log(`ERROR: ${error}`); 19 | return next({ 20 | log: 'Could not start minikube', 21 | message: `Error in starting minikube: ${error}`, 22 | }); 23 | }); 24 | 25 | } else { 26 | console.log('Minikube already started'); 27 | return next(); 28 | }; 29 | }; 30 | 31 | controller.deploymentYaml = async function(req, res, next) { 32 | try { 33 | let { clusterName, replicas, image, port, label } = req.body; 34 | 35 | const doc = await yaml.load(fs.readFileSync('./deployment-template.yaml', 'utf8')); 36 | console.log('DOC', doc.metadata.labels); 37 | 38 | //Set name 39 | doc.metadata.name = `${clusterName}`; 40 | 41 | //Set number of replicas 42 | doc.spec.replicas = replicas; 43 | 44 | // App and name labels, all use the same label 45 | doc.metadata.labels.app = label; 46 | doc.spec.selector.matchLabels.app = label; 47 | doc.spec.template.metadata.labels.app = label; 48 | doc.spec.template.spec.containers[0].name = label; 49 | 50 | //Set Docker image 51 | doc.spec.template.spec.containers[0].image = image; 52 | 53 | //Set Docker container app port 54 | doc.spec.template.spec.containers[0].ports[0].containerPort = port; 55 | 56 | console.log('DOC AFTER', doc); 57 | 58 | //Convert doc to YAML 59 | const newDoc = yaml.dump(doc); 60 | console.log('NEW DOC', newDoc); 61 | 62 | //Write to new YAML file 63 | fs.writeFile('./deployment.yaml', newDoc, err => { 64 | if (err) { 65 | next(err); 66 | }; 67 | }); 68 | 69 | return next(); 70 | } catch (err) { 71 | return next({ 72 | log: 'Couldn\'t update Deplyoment YAML file', 73 | message: { err: 'Error occurred in controller.deploymentYaml ' + err }, 74 | }); 75 | }; 76 | }; 77 | 78 | //Deployment of YAML 79 | controller.deploy = function(req, res, next) { 80 | exec('kubectl apply -f ./deployment.yaml', (err, stdout, stderr) => { 81 | if (err) { 82 | return next({ 83 | log: 'Couldn\'t Deploy YAML file', 84 | message: { err: 'Error occurred in controller.deploy ' + err }, 85 | }); 86 | } else { 87 | console.log(`THE DEPLOY OUTPUT ${stdout}`); 88 | res.locals.deployOutput = stdout; 89 | return next(); 90 | }; 91 | }); 92 | }; 93 | 94 | //Tunnel is required for minikube only to provide external IP to access deployed app 95 | controller.tunnel = function(req, res, next) { 96 | //Execute tunnel if it doesn't exists yet 97 | if(!tunnelProcess) { 98 | tunnelProcess = spawn('minikube', ['tunnel']); 99 | tunnelProcess.stdout.on('data', (data) => { 100 | console.log('STARTED TUNNEL'); 101 | return next(); 102 | }); 103 | tunnelProcess.on('error', (error) => { 104 | console.log(`ERROR: ${error}`); 105 | return next({ 106 | log: 'Could not create tunnel', 107 | message: `Error in creating tunnel: ${error}`, 108 | }); 109 | }); 110 | 111 | } else { 112 | console.log('TUNNEL already exists'); 113 | return next(); 114 | }; 115 | }; 116 | 117 | //Kill tunnel child process 118 | controller.killTunnel = function(req, res, next) { 119 | if(tunnelProcess) { 120 | tunnelProcess.kill(); 121 | tunnelProcess = null; 122 | console.log('Tunnel process is killed'); 123 | }; 124 | return next(); 125 | }; 126 | 127 | //Create load balancer and expose app on port 9000 128 | controller.expose = async function(req, res, next) { 129 | const doc = await yaml.load(fs.readFileSync('./deployment.yaml', 'utf8')); 130 | const clusterName = doc.metadata.name; 131 | const targetPort = doc.spec.template.spec.containers[0].ports[0].containerPort; 132 | console.log('TARGET PORT', targetPort); 133 | 134 | exec(`kubectl expose deployment ${clusterName} --type LoadBalancer --port=9000 --target-port ${targetPort}`, 135 | (err, stdout, stderr) => { 136 | if (err) { 137 | return next({ 138 | log: 'Couldn\'t Expose Deployment', 139 | message: { err: 'Error occurred in controller.expose ' + err }, 140 | }); 141 | } else { 142 | console.log(`Exposed ${stdout}`); 143 | res.locals.exposedOutput = stdout; 144 | return next(); 145 | }; 146 | }); 147 | }; 148 | 149 | 150 | module.exports = controller; -------------------------------------------------------------------------------- /server/controllers/googleController.js: -------------------------------------------------------------------------------- 1 | const { exec, spawn } = require('child_process'); 2 | const yaml = require('js-yaml'); 3 | const fs = require('fs'); 4 | 5 | const clusterOutputToObj = (string) => { 6 | // Takes string, strips whitespace and line breaks returns an array of only the strings 7 | const finalArr = []; 8 | let arr = string.split('\n').join(' ').split(' '); 9 | arr.forEach(ele => { if (ele !== '') {finalArr.push(ele)}}); 10 | 11 | let finalObj = {}; 12 | let finalRes = []; 13 | // Object to check if the first char of the next column is a "number" string 14 | const nums = { 15 | 0: '0', 16 | 1: '1', 17 | 2: '2', 18 | 3: '3', 19 | 4: '4', 20 | 5: '5', 21 | 6: '6', 22 | 7: '7', 23 | 8: '8', 24 | 9: '9', 25 | }; 26 | 27 | // Creates an arr of the row header values 28 | let endOfRow = finalArr.indexOf("STATUS"); 29 | const rowArr = []; 30 | for (let i = 0; i <= endOfRow; i++) rowArr.push(finalArr[i]); 31 | 32 | // Iterates over the array of string values, starting at the first non-header string 33 | let tally = 0; 34 | for (let i = endOfRow + 1; i < finalArr.length; i++) { 35 | const ele = finalArr[i]; 36 | const rowHead = rowArr[tally]; 37 | console.log('ele', ele, 'rowhead', rowHead) 38 | 39 | // Logic to make sure that MASTER_IP and NUM_NODES fields aren't empty 40 | if(rowHead === 'MASTER_IP') { 41 | if (nums[ele[0]]) { 42 | finalObj[rowHead] = ele; 43 | } else { 44 | finalObj[rowHead] = 'undefined'; 45 | finalObj[rowArr[tally + 1]] = ele; 46 | i - 1; 47 | tally++; 48 | }; 49 | } else if(rowHead === 'NUM_NODES') { 50 | if (ele.length < 3) { 51 | finalObj[rowHead] = ele; 52 | } else { 53 | finalObj[rowHead] = 'undefined'; 54 | finalObj[rowArr[tally + 1]] = ele; 55 | i - 1; 56 | tally++; 57 | }; 58 | } else { 59 | finalObj[rowHead] = ele; 60 | }; 61 | 62 | // Logic to create a new object of key value pairs, and push the current object to our final array 63 | if(Object.keys(finalObj).length === endOfRow + 1) { 64 | finalRes.push(finalObj); 65 | finalObj = {}; 66 | tally = 0; 67 | } else { 68 | tally++; 69 | }; 70 | }; 71 | 72 | // Logic to deal with any remaining object that wasn't pushed to the result array 73 | if (Object.keys(finalObj).length !== 0) { 74 | finalRes.push(finalObj) 75 | }; 76 | return finalRes; 77 | }; 78 | 79 | const projectsOutputToObj = (string) => { 80 | const finalArr = []; 81 | let arr = string.split('\n').join(' ').split(' '); 82 | arr.forEach(ele => { if (ele !== '') {finalArr.push(ele)}}); 83 | 84 | let endOfRow = finalArr.indexOf("PROJECT_NUMBER"); 85 | const rowArr = []; 86 | for (let i = 0; i <= endOfRow; i++) rowArr.push(finalArr[i]); 87 | 88 | let finalObj = {}; 89 | let finalRes = []; 90 | 91 | for (let i = endOfRow + 1; i < finalArr.length; i++) { 92 | const ele = finalArr[i]; 93 | const rowHead = rowArr[i % rowArr.length]; 94 | 95 | if (finalObj[rowHead]) { 96 | finalRes.push(finalObj) 97 | finalObj = {}; 98 | } 99 | finalObj[rowHead] = ele; 100 | } 101 | 102 | if (Object.keys(finalObj).length !== 0) { 103 | finalRes.push(finalObj) 104 | }; 105 | 106 | return finalRes; 107 | }; 108 | 109 | 110 | const googleController = {}; 111 | 112 | googleController.getProjects = (req, res, next) => { 113 | console.log('MADE IT TO GET PROJECTS') 114 | 115 | exec(`gcloud projects list`, (err, stdout, stderr) => { 116 | projectsOutputToObj(stdout) 117 | if (err) { 118 | return next({ 119 | log: 'Couldn\'t get Project List', 120 | message: { err: 'Error occurred in googleController.getProjects ' + err }, 121 | }); 122 | } else { 123 | res.locals.googleGetProjects = projectsOutputToObj(stdout); 124 | console.log(res.locals.googleGetProjects) 125 | } 126 | return next(); 127 | }); 128 | }; 129 | 130 | googleController.selectProject = (req, res, next) => { 131 | // console.log('MADE IT TO SELECT PROJECTS') 132 | // console.log('REQ BODY', req.body) 133 | const { projectID } = req.body; 134 | 135 | exec(`gcloud config set project ${projectID}`, (err, stdout, stderr) => { 136 | console.log('STDOUT', stdout) 137 | console.log('stderr', stderr) 138 | console.log('STDOUT', err) 139 | 140 | if (err) { 141 | return next({ 142 | log: 'Couldn\'t Select Project', 143 | message: { err: 'Error occurred in googleController.selectProject ' + err }, 144 | }); 145 | } else { 146 | res.locals.googleSelectProject = stderr; 147 | } 148 | return next(); 149 | }); 150 | }; 151 | 152 | googleController.createCluster = (req, res, next) => { 153 | const { clusterName } = req.body; 154 | 155 | exec(`gcloud container clusters create-auto ${clusterName} \ 156 | --location=us-central1`, (err, stdout, stderr) => { 157 | if (err) { 158 | return next({ 159 | log: 'Couldn\'t create Google Cluster', 160 | message: { err: 'Error occurred in googleController.createCluster ' + err }, 161 | }); 162 | } else { 163 | res.locals.googleCreateClusterOutput = stdout; 164 | } 165 | return next(); 166 | }); 167 | }; 168 | 169 | googleController.getClusters = (req, res, next) => { 170 | console.log('made it to get clusters') 171 | 172 | exec(`gcloud container clusters list`, (err, stdout, stderr) => { 173 | if (err) { 174 | return next({ 175 | log: 'Couldn\'t get clusters', 176 | message: { err: 'Error occurred in googleController.getClusters ' + err }, 177 | }); 178 | } else { 179 | if (stdout === null) { 180 | res.locals.getClusters = 'No Clusters Found' 181 | return next(); 182 | } 183 | res.locals.getClusters = clusterOutputToObj(stdout); 184 | } 185 | return next(); 186 | }); 187 | }; 188 | 189 | googleController.getCredentials = async (req, res, next) => { 190 | const { clusterName, location } = req.body; 191 | 192 | // console.log(req.body); 193 | console.log("inside of getCredentials SERVER SIDE", clusterName); 194 | 195 | // TIES YOUR 'KUBECTL' COMMAND TO THE GOOGLE CLOUD CLUSTER 196 | await exec(`gcloud container clusters get-credentials ${clusterName} --location ${location}`, (err, stderr, stdout) => { 197 | if (err) { 198 | return next({ 199 | log: 'Couldn\'t get Google Credentials', 200 | message: { err: 'Error occurred in googleController.getCredentials ' + err }, 201 | }); 202 | } else { 203 | res.locals.getCreds = stdout; 204 | }; 205 | return next(); 206 | }); 207 | }; 208 | 209 | googleController.deploy = (req, res, next) => { 210 | const { clusterName, image } = req.body; 211 | 212 | // Why "kubectl create deployment" over kubectl apply? 213 | // => convenience... 214 | // this generates a default YAML file for deployment (NOT a customized one) 215 | 216 | exec(`kubectl create deployment ${clusterName} \ 217 | --image=${image}`, (err, stdout, stderr) => { 218 | if (err) { 219 | return next({ 220 | log: 'Couldn\'t deploy to Google Cloud', 221 | message: { err: 'Error occurred in googleController.deploy ' + err }, 222 | }); 223 | } else { 224 | res.locals.googleDeploy = stdout; 225 | } 226 | return next(); 227 | }); 228 | }; 229 | 230 | googleController.getEndpoint = async (req, res, next) => { 231 | const doc = await yaml.load(fs.readFileSync('./deployment.yaml', 'utf8')); 232 | const clusterName = doc.metadata.name; 233 | exec(`kubectl get services ${clusterName} -o jsonpath='{.status.loadBalancer.ingress[0].ip}:{.spec.ports[0].port}'`, (err, stdout, stderr) => { 234 | console.log('STDOUT', stdout) 235 | console.log('stderr', stderr) 236 | console.log('STDOUT', err) 237 | if (err) { 238 | return next({ 239 | log: 'Error in getEndpoint func', 240 | message: { err: 'Error occurred in googleController.getEndpoint ' + err }, 241 | }); 242 | } else { 243 | // console.log('STDOUT GET ENDPOINT', stdout) 244 | res.locals.endpoint = stdout; 245 | }; 246 | return next(); 247 | }); 248 | }; 249 | 250 | googleController.testFunc = (req, res, next) => { 251 | exec(`gcloud auth login`, (err, stdout, stderr) => { 252 | console.log('STDOUT', stdout) 253 | console.log('stderr', stderr) 254 | console.log('STDOUT', err) 255 | 256 | if (err) { 257 | return next({ 258 | log: 'Error in test func', 259 | message: { err: 'Error occurred in googleController.testFunc ' + err }, 260 | }); 261 | } else { 262 | res.locals.test = stdout; 263 | }; 264 | return next(); 265 | }); 266 | }; 267 | 268 | module.exports = googleController; -------------------------------------------------------------------------------- /server/controllers/statusController.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const yaml = require('js-yaml'); 3 | const fs = require('fs'); 4 | 5 | const statusController = {}; 6 | 7 | statusController.getDeployment = async (req, res, next) => { 8 | const doc = await yaml.load(fs.readFileSync('./deployment.yaml', 'utf8')); 9 | const imageName = doc.spec.template.spec.containers[0].image; 10 | 11 | exec('kubectl get deployments', (err, stdout, stderr) => { 12 | if (err) { 13 | return next({ 14 | log: 'Couldn\'t get deployments', 15 | message: { err: 'Error occurred in statusController.getDeployment: ' + err }, 16 | }); 17 | } else { 18 | console.log(`THE GET DEPLOYMENT OUTPUT ${stdout}`); 19 | res.locals.getDeploymentOutput = stdout.concat(imageName); 20 | return next(); 21 | }; 22 | }); 23 | }; 24 | 25 | statusController.getService = (req, res, next) => { 26 | exec(`kubectl get services`, (err, stdout, stderr) => { 27 | if (err) { 28 | return next({ 29 | log: 'Couldn\'t Get Service', 30 | message: { err: 'Error occurred in statusController.getService ' + err }, 31 | }); 32 | } else { 33 | console.log(`THE GET SERVICES OUTPUT ${stdout}`); 34 | res.locals.serviceOutput = stdout; 35 | return next(); 36 | }; 37 | }); 38 | }; 39 | 40 | statusController.getPods = (req, res, next) => { 41 | exec('kubectl get pods', (err, stdout, stderr) => { 42 | if (err) { 43 | return next({ 44 | log: 'Couldn\'t get pods', 45 | message: { err: 'Error occurred when getting pods: ' + err }, 46 | }); 47 | } else { 48 | console.log(`THE GET PODS OUTPUT ${stdout}`); 49 | res.locals.getPodsOutput = stdout; 50 | return next(); 51 | }; 52 | }); 53 | }; 54 | 55 | statusController.deleteService = async (req, res, next) => { 56 | const doc = await yaml.load(fs.readFileSync('./deployment.yaml', 'utf8')); 57 | const clusterName = doc.metadata.name; 58 | console.log('CLUSTER NAME ', clusterName); 59 | 60 | exec(`kubectl delete service ${clusterName}`, (err, stdout, stderr) => { 61 | if (err) { 62 | return next({ 63 | log: 'Couldn\'t Delete Service', 64 | message: { err: `Error occurred in statusController.deleteService: ${err}` }, 65 | }); 66 | } else { 67 | console.log('OUTPUT FROM DELETE SERVICE ', stdout); 68 | res.locals.deleteService = stdout; 69 | return next(); 70 | } 71 | }); 72 | }; 73 | 74 | statusController.deleteDeployment = async function(req, res, next) { 75 | const doc = await yaml.load(fs.readFileSync('./deployment.yaml', 'utf8')); 76 | const clusterName = doc.metadata.name; 77 | 78 | exec(`kubectl delete deployment ${clusterName}`, (err, stdout, stderr) => { 79 | if (err) { 80 | return next({ 81 | log: 'Couldn\'t Delete Deployment', 82 | message: { err: `Error occurred in statusController.deleteDeployment: ${err}` }, 83 | }); 84 | } else { 85 | console.log(stdout); 86 | res.locals.deleteDeployment = stdout; 87 | return next(); 88 | } 89 | }); 90 | }; 91 | 92 | module.exports = statusController; -------------------------------------------------------------------------------- /server/express.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const cors = require('cors'); 5 | 6 | const router = require('./routers/router.js'); 7 | const googleRouter = require('./routers/googleRouter.js'); 8 | const statusRouter = require('./routers/statusRouter.js'); 9 | 10 | app.use(express.json()); 11 | app.use(cors()); 12 | 13 | app.use(express.static(path.join(__dirname, '../build'))); 14 | 15 | // app.get('/*', (req, res) => { 16 | // res.sendFile(path.join(__dirname, '../dist', 'index.html')) 17 | // }); 18 | 19 | // Routers 20 | app.use('/api', router); 21 | app.use('/google', googleRouter); 22 | app.use('/status', statusRouter); 23 | 24 | // 404 Error Handler 25 | app.use('*', (req,res) => { 26 | res.status(404).send('Page not found.'); 27 | }); 28 | 29 | // Global Erorr Handler 30 | app.use((err, req, res, next) => { 31 | const defaultErr = { 32 | log: 'Express default error handler', 33 | status: 500, 34 | message: {error: `An error occurred: ${err}`} 35 | }; 36 | 37 | const errorObj = Object.assign({}, defaultErr, err); 38 | 39 | return res.status(errorObj.status).json(errorObj.message); 40 | }); 41 | 42 | 43 | const PORT = process.env.PORT || 3001; 44 | 45 | app.listen(PORT, () => { 46 | console.log(`App is listening on`, PORT); 47 | }); 48 | 49 | module.exports = app; -------------------------------------------------------------------------------- /server/routers/googleRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const googleRouter = express.Router(); 3 | const googleController = require('../controllers/googleController.js'); 4 | 5 | googleRouter.use('/getProjects', googleController.getProjects, (req, res, next) => { 6 | console.log('Made it past get projects middleware'); 7 | return res.status(200).json(res.locals.googleGetProjects); 8 | }); 9 | 10 | googleRouter.use('/selectProject', googleController.selectProject, (req, res, next) => { 11 | console.log('Made it past select project middleware'); 12 | return res.status(200).json(`${res.locals.googleSelectProject}, Project was selected!`); 13 | }); 14 | 15 | googleRouter.use('/createCluster', googleController.createCluster, (req, res, next) => { 16 | // console.log('Made it past create cluster middleware'); 17 | return res.status(200); 18 | }); 19 | 20 | googleRouter.use('/getClusters', googleController.getClusters, (req, res, next) => { 21 | console.log('Made it past get cluster middleware'); 22 | return res.status(200).json(res.locals.getClusters); 23 | }); 24 | 25 | googleRouter.use('/getCredentials', googleController.getCredentials, (req, res, next) => { 26 | // console.log('Made it past getCredentials middleware', res.locals.getCreds); 27 | return res.status(200).json(res.locals.getCreds); 28 | }); 29 | 30 | googleRouter.use('/deploy', googleController.deploy, (req, res, next) => { 31 | // console.log('Made it past deploy middleware'); 32 | return res.status(200); 33 | }); 34 | 35 | googleRouter.use('/getEndpoint', googleController.getEndpoint, (req, res) => { 36 | // console.log('Made it past test middleware'); 37 | return res.status(200).json(res.locals.endpoint); 38 | }); 39 | 40 | googleRouter.use('/test', googleController.testFunc, (req, res, next) => { 41 | // console.log('Made it past test middleware'); 42 | return res.status(200).json(res.locals.test); 43 | }); 44 | 45 | module.exports = googleRouter; -------------------------------------------------------------------------------- /server/routers/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const controller = require('../controllers/controller.js'); 4 | 5 | 6 | router.use('/startminikube', controller.startMinikube, (req, res) => { 7 | 8 | return res.status(200).json('Minikube started') 9 | }); 10 | 11 | router.use('/yaml', controller.deploymentYaml, (req, res) => { 12 | 13 | return res.status(200).json('Deployment YAML Created') 14 | }); 15 | 16 | 17 | router.use('/deploy', controller.deploy, (req, res) => { 18 | 19 | return res.status(200).json(`Cluster Deployed Status: ${res.locals.deployOutput}`) 20 | }); 21 | 22 | 23 | router.use('/tunnelexpose', controller.tunnel, controller.expose, (req, res) => { 24 | return res.status(200).json(`Exposure: ${res.locals.exposedOutput}`) 25 | }); 26 | 27 | 28 | router.use('/expose', controller.expose, (req, res) => { 29 | return res.status(200).json(`Exposure: ${res.locals.exposedOutput}`) 30 | }); 31 | 32 | 33 | module.exports = router; -------------------------------------------------------------------------------- /server/routers/statusRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const statusRouter = express.Router(); 3 | const statusController = require('../controllers/statusController.js'); 4 | 5 | statusRouter.use('/getDeployment', statusController.getDeployment, (req, res) => { 6 | 7 | return res.status(200).json(`Deployments: ${res.locals.getDeploymentOutput}`); 8 | }); 9 | 10 | 11 | statusRouter.use('/getService', statusController.getService, (req, res) => { 12 | 13 | return res.status(200).json(`Service information: ${res.locals.serviceOutput}`); 14 | }); 15 | 16 | 17 | statusRouter.use('/getPods', statusController.getPods, (req, res) => { 18 | 19 | return res.status(200).json(`Pods: ${res.locals.getPodsOutput}`); 20 | }); 21 | 22 | statusRouter.use('/deleteService', statusController.deleteService, (req, res) => { 23 | 24 | return res.status(200).json(`Service Deleted: ${res.locals.deleteService}`); 25 | }); 26 | 27 | 28 | statusRouter.use('/deleteDeployment', statusController.deleteDeployment, (req, res) => { 29 | 30 | return res.status(200).json(`Deployment deleted: ${res.locals.deleteDeployment}`); 31 | }); 32 | 33 | 34 | statusRouter.use('/delete', statusController.deleteService, statusController.deleteDeployment, (req, res) => { 35 | return res.status(200).json(res.locals) 36 | }); 37 | 38 | 39 | module.exports = statusRouter; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: [ 6 | './client/index.js' 7 | ], //this is where webpack looks for the root client/index.js 8 | output: { 9 | path: path.resolve(__dirname, 'build'), // dist is common practice 10 | publicPath: '/', // this means it starts at the simplest version of the url 11 | filename: 'bundle.js' // what the bundle file will be called 12 | }, 13 | plugins: [new HtmlWebpackPlugin({ 14 | template: './client/index.html' 15 | })], // the plugin generates a new HTML5 file for you with all your wepback bundles using script tags 16 | resolve: { 17 | // Enable importing JS / JSX files without specifying their extension 18 | extensions: ['.js', '.jsx'], 19 | modules: [path.resolve(__dirname, 'client'), 'node_modules'], 20 | }, 21 | mode: 'development', 22 | devServer: { 23 | host: 'localhost', 24 | port: process.env.FRONTEND_PORT || 8090, 25 | static: { 26 | directory: path.join(__dirname, '/build'), 27 | publicPath: '/build/bundle.js' 28 | }, 29 | hot: true, 30 | proxy: { 31 | '/api/**': { 32 | target: 'http://localhost:3001/', 33 | secure: false, 34 | }, 35 | '/status/**': { 36 | target: 'http://localhost:3001/', 37 | secure: false, 38 | }, 39 | '/google/**': { 40 | target: 'http://localhost:3001/', 41 | secure: false, 42 | }, 43 | }, 44 | historyApiFallback: true, 45 | }, 46 | module: { 47 | // rules and properties for how to deal with our files 48 | rules: [ 49 | { 50 | test: /\.jsx?/, // this is a regex expression and it's checking if the file is jsx 51 | exclude: /node_modules/, // you don't want to bundle any of those files 52 | use: { 53 | loader: 'babel-loader', 54 | options: { 55 | presets: [ 56 | ['@babel/preset-env', { targets: "defaults" }], 57 | ['@babel/preset-react', { targets: "defaults" }] 58 | ] 59 | } 60 | } 61 | }, 62 | { 63 | test: /\.css$/, 64 | exclude: /node_modules/, 65 | use: [ 66 | // creates style nodes from JS strings - ORDER MATTERS! 67 | // these loaders are used in backwards order 68 | 'style-loader', 69 | 'css-loader' 70 | ] 71 | }, 72 | { 73 | test: /\.(png|jpe?g|gif|svg)$/, 74 | use: [ 75 | { 76 | loader: 'file-loader', 77 | options: { 78 | name: '[name].[ext]', 79 | outputPath: 'assets/', 80 | } 81 | } 82 | ] 83 | }, 84 | { 85 | test: /\.ya?ml$/, 86 | use: 'yaml-loader' 87 | } 88 | ] 89 | }, 90 | }; 91 | --------------------------------------------------------------------------------