├── .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 |
5 |
6 | Tech Stack
7 |
8 |
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 |
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 |
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 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Tarik Bensalem
82 | Rita Bizhan
83 | Jeff Chan
84 | Cristina Flores
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
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 |
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 |
169 | Continue
170 |
171 |
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 | Google Cloud CLI installed on your computer
95 | Kubectl authentication for your Google Cloud account
96 | A containerized application
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 |
141 | Let's go!
142 |
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 = More info ;
38 | const showLessButton = Less info ;
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 |
67 | Select
68 |
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 | {handlePostYaml(e)}}>Generate YAML
355 | {handleExpose(e)}}>Expose
356 | {handleDeploy(e)}}>Deploy
357 | See deployments
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 |
{handleEnvironmentChange('minikube')}}>
34 |
38 |
39 |
40 |
41 |
42 | {/* CLOUD CONTAINER */}
43 |
44 |
51 |
handleEnvironmentChange('cloud')}>
52 |
56 |
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 | A container or virtual machine manager
67 | Minikube installed on your machine
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 |
99 | Let's go!
100 |
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 | Select
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 |
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 | {handleDelete(e)}}>Delete Deployment
202 | {handleGetEndPoint(e)}}>Launch app
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 |
--------------------------------------------------------------------------------