├── public ├── LogoPNG.png ├── favicon.ico ├── assets │ └── banner.png ├── backgroundImg.png ├── setupExs │ ├── containers.png │ ├── visit-9090.png │ ├── apply-restart.png │ ├── helm-repo-add.png │ ├── helm-repo-update.png │ ├── kubectl-get-pod.png │ ├── kubectl-get-pods.png │ ├── kubectl-version.png │ ├── minikube-start.jpg │ ├── status-targets.png │ ├── brew-install-helm.png │ ├── kubectl-get-nodes.png │ ├── helm-install-cluster.png │ ├── kubectl-get-services.png │ ├── kubectl-port-forward.png │ └── brew-install-prometheus.png ├── vercel.svg ├── LICENSE └── LOGO-FINAL.svg ├── postcss.config.js ├── pages ├── auth │ ├── style.css │ └── signin.tsx ├── _app.tsx ├── api │ ├── user │ │ ├── login.ts │ │ └── register.ts │ └── auth │ │ └── [...nextauth].ts ├── index.tsx └── register.tsx ├── cypress ├── fixtures │ └── example.json ├── support │ ├── e2e.ts │ └── commands.ts └── e2e │ └── spec.cy.ts ├── cypress.config.ts ├── components ├── dashboard │ ├── costError.tsx │ ├── spinner.tsx │ ├── grafana.tsx │ ├── dashboardContainer.tsx │ └── costComponent.tsx ├── input.tsx ├── navbar.tsx ├── login-btn.tsx ├── footer.tsx └── register.tsx ├── models └── user.ts ├── old_env ├── .gitignore ├── tailwind.config.js ├── tsconfig.json ├── utils └── connectMongo.ts ├── typings.d.ts ├── test.txt ├── next.config.js ├── package.json ├── prometheus-prometheus.yaml ├── styles └── globals.css ├── copy.md ├── constants.ts └── README.md /public/LogoPNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/LogoPNG.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/assets/banner.png -------------------------------------------------------------------------------- /public/backgroundImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/backgroundImg.png -------------------------------------------------------------------------------- /public/setupExs/containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/containers.png -------------------------------------------------------------------------------- /public/setupExs/visit-9090.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/visit-9090.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/setupExs/apply-restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/apply-restart.png -------------------------------------------------------------------------------- /public/setupExs/helm-repo-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/helm-repo-add.png -------------------------------------------------------------------------------- /public/setupExs/helm-repo-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/helm-repo-update.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-get-pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-get-pod.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-get-pods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-get-pods.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-version.png -------------------------------------------------------------------------------- /public/setupExs/minikube-start.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/minikube-start.jpg -------------------------------------------------------------------------------- /public/setupExs/status-targets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/status-targets.png -------------------------------------------------------------------------------- /public/setupExs/brew-install-helm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/brew-install-helm.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-get-nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-get-nodes.png -------------------------------------------------------------------------------- /public/setupExs/helm-install-cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/helm-install-cluster.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-get-services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-get-services.png -------------------------------------------------------------------------------- /public/setupExs/kubectl-port-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/kubectl-port-forward.png -------------------------------------------------------------------------------- /public/setupExs/brew-install-prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/poseidon/HEAD/public/setupExs/brew-install-prometheus.png -------------------------------------------------------------------------------- /pages/auth/style.css: -------------------------------------------------------------------------------- 1 | .body { 2 | background-image: url('../public/backgroundImg.png'); 3 | background-repeat: no-repeat; 4 | backdrop-filter: blur(100px); 5 | } 6 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on:any, config:any) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /components/dashboard/costError.tsx: -------------------------------------------------------------------------------- 1 | export default function CostError() { 2 | return ( 3 |
4 | You need to add the Kubecost IP address to your .env.local file to see 5 | this data! 6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /components/dashboard/spinner.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Logo from '../../public/LOGO-FINAL.svg'; 3 | 4 | export default function Spinner() { 5 | return ( 6 |
7 | Logo 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | export default function MyApp({ 6 | Component, 7 | pageProps: { session, ...pageProps }, 8 | }: AppProps) { 9 | return ( 10 | /* A wrapper for the next-auth library. */ 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /models/user.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, models } from 'mongoose'; 2 | 3 | const userSchema = new Schema({ 4 | email: { type: String, require: true, unique: true }, 5 | password: { type: String, require: true }, 6 | firstName: { type: String, require: true }, 7 | lastName: { type: String, require: true }, 8 | ipAddress: { type: String, require: false }, 9 | }); 10 | 11 | const User = models.User || model('User', userSchema); 12 | 13 | export default User; 14 | -------------------------------------------------------------------------------- /old_env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | MONGO_URI="mongodb+srv://xtchow:poseidon@cluster0.lkfnb5l.mongodb.net/?retryWrites=true&w=majority" 3 | NEXTAUTH_URL=http://localhost:3500 4 | BCRYPT_SALT=10 5 | NEXTAUTH_SECRET=12345678910 6 | LOCAL_CLUSTER_IP = "localhost:3300" 7 | DEPLOYED_CLUSTER_IP = "34.28.48.106" 8 | LOCAL_CLUSTER_NAME = "Jd5xvc04z" 9 | DEPLOYED_CLUSTER_NAME = "3UwLYP04k" 10 | DEPLOYED_CLUSTER_NAME_2 = "eYlLLE0Vk" 11 | KUBECOST_IP = "34.121.148.52" 12 | LOCAL_KUBECOST_IP = "127.0.0.1" -------------------------------------------------------------------------------- /components/dashboard/grafana.tsx: -------------------------------------------------------------------------------- 1 | export default function Grafana(props: any) { 2 | const { url } = props; 3 | return ( 4 |
5 | 6 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | './app/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | borderWidth: { 10 | DEFAULT: '0.2px', 11 | }, 12 | }, 13 | fontFamily: { 14 | sans: ['Inter var, sans-serif', { fontFeatureSettings: '"cv11", "ss01"' }], 15 | }, 16 | theme: { 17 | container: { 18 | center: true, 19 | }, 20 | }, 21 | plugins: [require('flowbite/plugin')], 22 | plugins: [], 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /utils/connectMongo.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import getConfig from 'next/config' 3 | 4 | /* Mongoose has a strict query mode that is enabled by default. This mode requires that you specify a 5 | query filter for all update() and remove() operations. */ 6 | mongoose.set('strictQuery', false); 7 | 8 | /* It's getting the server runtime config from the `next.config.js` file. */ 9 | const { serverRuntimeConfig } = getConfig() 10 | 11 | /** 12 | * It connects to MongoDB using the URI stored in the serverRuntimeConfig object 13 | */ 14 | const connectMongo = async () => mongoose.connect(serverRuntimeConfig.MONGO_URI); 15 | 16 | export default connectMongo; -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | interface LoginValues { 2 | email: string; 3 | password: string; 4 | } 5 | interface KubecostTotals { 6 | totalCPU: number; 7 | totalRAM: number; 8 | totalGPU: number; 9 | totalloadBalancerCost: number; 10 | totalNetworkCost: number; 11 | totalPVCost: number; 12 | totalCost: number; 13 | totalSharedCost: number; 14 | totalDays: number; 15 | key: string; 16 | } 17 | interface HomeProps { 18 | data: {}[]; 19 | } 20 | interface RegisterValues { 21 | email: string; 22 | password: string; 23 | lastName: string; 24 | firstName: string; 25 | ipAddress: string; 26 | changePassword?: string; 27 | } 28 | interface SessionData { 29 | email: string; 30 | lastName: string; 31 | firstName: string; 32 | ipAddress: string; 33 | } -------------------------------------------------------------------------------- /test.txt: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | MONGO_URI="[your mongo db link here]" 3 | NEXTAUTH_URL=http://localhost:3500 4 | BCRYPT_SALT=10 5 | NEXTAUTH_SECRET=[your secret here (any combination of letters or numbers)] 6 | LOCAL_CLUSTER_IP = [your grafana link here: e.g. "localhost:3300"] 7 | DEPLOYED_CLUSTER_IP = [your cloud-deployed ip address here, e.g. "0.0.0.0"] 8 | LOCAL_CLUSTER_NAME = [your local cluster's grafana embed link name (.../d-solo/__your_name__/...)] 9 | DEPLOYED_CLUSTER_NAME = [your deployed cluster's grafana embed link name (.../d-solo/__your_name__/...)] 10 | DEPLOYED_CLUSTER_NAME_2 = [OPTIONAL: any other node in your deployed cluster's grafana embed link name (.../d-solo/__your_name__/...)] 11 | KUBECOST_IP = [your cloud-deployed kubecost ip address here, e.g. "0.0.0.0"] 12 | LOCAL_KUBECOST_IP = [your local kubecost ip address here, e.g. "0.0.0.0"] -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | serverRuntimeConfig: { 4 | // Will only be available on the server side 5 | MONGO_URI: process.env.MONGO_URI, 6 | // DEPLOYED_CLUSTER_IP: process.env.DEPLOYED_CLUSTER_IP, // Pass through env variables 7 | 8 | // DEPLOYED_CLUSTER_NAME: process.env.DEPLOYED_CLUSTER_NAME, 9 | }, 10 | publicRuntimeConfig: { 11 | // Will be available on both server and client 12 | KUBECOST_IP: process.env.KUBECOST_IP, 13 | LOCAL_CLUSTER_IP: process.env.LOCAL_CLUSTER_IP, 14 | LOCAL_CLUSTER_NAME: process.env.LOCAL_CLUSTER_NAME, 15 | DEPLOYED_CLUSTER_IP: process.env.DEPLOYED_CLUSTER_IP, 16 | DEPLOYED_CLUSTER_NAME: process.env.DEPLOYED_CLUSTER_NAME, 17 | DEPLOYED_CLUSTER_NAME_2: process.env.DEPLOYED_CLUSTER_NAME_2, 18 | LOCAL_KUBECOST_IP: process.env.LOCAL_KUBECOST_IP, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /components/input.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorMessage, useField } from "formik"; 2 | 3 | // input component used in forms for register 4 | export const Input = ({ name, label, ...props }: any) => { 5 | const [field, meta] = useField(name); 6 | return ( 7 |
8 | 15 | 20 |
21 | ); 22 | }; -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "port": "next dev -p 3500", 6 | "build": "next build", 7 | "start": "next start", 8 | "test": "npx kill-port 3500 && next dev -p 3500 & cypress open", 9 | "forward": "kubectl port-forward --namespace kubecost deployment/kubecost-cost-analyzer 9090 & next dev -p 3500 & kubectl port-forward svc/prometheus-operated 9000:9090 & kubectl --namespace default port-forward svc/minikube-grafana 3300:80" 10 | }, 11 | "dependencies": { 12 | "bcrypt": "^5.1.0", 13 | "bcryptjs": "^2.4.3", 14 | "colors": "^1.4.0", 15 | "flowbite": "^1.6.3", 16 | "formik": "^2.2.9", 17 | "mongoose": "^6.8.4", 18 | "next": "latest", 19 | "next-auth": "^4.19.2", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "swr": "^2.0.3", 23 | "yup": "^0.32.11" 24 | }, 25 | "devDependencies": { 26 | "@types/bcryptjs": "^2.4.2", 27 | "@types/node": "18.11.3", 28 | "@types/react": "18.0.21", 29 | "@types/react-dom": "18.0.6", 30 | "autoprefixer": "^10.4.12", 31 | "cypress": "^12.5.1", 32 | "postcss": "^8.4.18", 33 | "tailwindcss": "^3.2.4", 34 | "typescript": "4.9.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Image from 'next/image'; 3 | import Logo from '../public/LOGO-FINAL.svg'; 4 | import LoginButton from './login-btn'; 5 | 6 | export default function NavBar() { 7 | return ( 8 | <> 9 |
10 |
11 |
12 | 16 | {/* logo vector */} 17 | Logo 24 | 25 | POSEIDON 26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /prometheus-prometheus.yaml: -------------------------------------------------------------------------------- 1 | # for local clusters, applies grafana user keys and tokens 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: Prometheus 4 | metadata: 5 | labels: 6 | prometheus: k8s 7 | name: k8s 8 | namespace: default 9 | spec: 10 | alerting: 11 | alertmanagers: 12 | - name: alertmanager-main 13 | namespace: default 14 | port: web 15 | image: quay.io/prometheus/prometheus:v2.22.1 16 | nodeSelector: 17 | kubernetes.io/os: linux 18 | podMonitorNamespaceSelector: {} 19 | podMonitorSelector: {} 20 | probeNamespaceSelector: {} 21 | probeSelector: {} 22 | replicas: 2 23 | resources: 24 | requests: 25 | memory: 400Mi 26 | ruleSelector: 27 | matchLabels: 28 | prometheus: k8s 29 | role: alert-rules 30 | securityContext: 31 | fsGroup: 2000 32 | runAsNonRoot: true 33 | runAsUser: 1000 34 | serviceAccountName: prometheus-k8s 35 | serviceMonitorNamespaceSelector: {} 36 | serviceMonitorSelector: {} 37 | version: v2.22.1 38 | remoteWrite: 39 | - url: 'https://prometheus-us-central1.grafana.net/api/prom/push' 40 | basicAuth: 41 | username: 42 | name: kubepromsecret 43 | key: '750773' 44 | password: 45 | name: kubepromsecret 46 | key: 'eyJrIjoiOWVlMDRiMWI5NTNlYWM4ODY5MTQ5Zjk4MTE2YjRiNWJiYmEwMDQ0OSIsIm4iOiJQb3NlZGlvbiIsImlkIjo3ODQ5NjJ9' 47 | replicaExternalLabelName: '__replica__' 48 | externalLabels: { cluster: 'test' } 49 | -------------------------------------------------------------------------------- /pages/api/user/login.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import connectMongo from '../../../utils/connectMongo'; 3 | import User from '../../../models/user'; 4 | import type { NextApiRequest, NextApiResponse } from 'next'; 5 | import bcrypt from 'bcryptjs'; 6 | 7 | export default async function Login(req: NextApiRequest, res: NextApiResponse) { 8 | 9 | /* Connecting to the mongo database. */ 10 | await connectMongo(); 11 | 12 | 13 | /* Destructuring the request object. */ 14 | const { method, body } = req; 15 | const { email, password } = body; 16 | 17 | 18 | /* Checking to see if the email and password are present. */ 19 | if (email && password) { 20 | switch (method) { 21 | case 'POST': 22 | try { 23 | /* Finding the user in the database. */ 24 | const user = await User.findOne({ email }); 25 | 26 | // need to handle incorrect password 27 | if (user && (await bcrypt.compare(password, user.password))) { 28 | return res.json({ user }); 29 | } else { 30 | return res.json({ message: 'User not authorized.' }); 31 | } 32 | } catch (reason) { 33 | return res.status(500).json({ login: `Error in GET. ${reason}` }); 34 | } 35 | default: 36 | return res.status(200).json({ login: 'This route is not set up yet!' }); 37 | } 38 | } else { 39 | return res.status(400).json({ login: 'MISSING FEILD' }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import DashboardContainer from '../components/dashboard/dashboardContainer'; 3 | import NavBar from '../components/navbar'; 4 | import Footer from '../components/footer'; 5 | import { authOptions } from './api/auth/[...nextauth]'; 6 | import { getServerSession } from 'next-auth/next'; 7 | 8 | 9 | export default function Home() { 10 | return ( 11 |
12 | 13 | Dashboard 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | // this is a server side retreival of the session cookie, which then returns the props to the component above- allows the page to not even render if no cookie 28 | export async function getServerSideProps(context: any) { 29 | // this gets the session cookie from the server 30 | const session = await getServerSession(context.req, context.res, authOptions); 31 | 32 | // if there is no session cookie, redirect them to the signin page - well before page renders 33 | if (!session) { 34 | return { 35 | redirect: { 36 | destination: '/auth/signin', 37 | permanent: false, 38 | }, 39 | }; 40 | } 41 | return { 42 | props: { session }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/user/register.ts: -------------------------------------------------------------------------------- 1 | import connectMongo from '../../../utils/connectMongo'; 2 | import User from '../../../models/user'; 3 | import type { NextApiRequest, NextApiResponse } from 'next'; 4 | import bcrypt from 'bcryptjs'; 5 | 6 | export default async function Register( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | await connectMongo(); 11 | const { method, body } = req; 12 | const { email, password, firstName, lastName, ipAddress} = body; 13 | if (email && password && firstName && lastName && ipAddress) { 14 | switch (method) { 15 | case 'POST': 16 | try { 17 | //Check if user is already in db 18 | const userExist = await User.findOne({ email }); 19 | 20 | //hash password for create user 21 | const salt = await bcrypt.genSalt(10); 22 | 23 | // hash the pasword 24 | const hashedPassword: string = await bcrypt.hash(password, salt); 25 | if (userExist) { 26 | return res.json({ user: false }); 27 | } else { 28 | const user = await User.create({ 29 | email, 30 | password: hashedPassword, 31 | firstName, 32 | lastName, 33 | ipAddress, 34 | }); 35 | return res.json({ user }); 36 | } 37 | 38 | } catch (reason) { 39 | return res.status(500).json({ login: `Error in GET. ${reason}` }); 40 | } 41 | break; 42 | default: 43 | return res 44 | .status(200) 45 | .json({ register: 'This route is not set up yet!' }); 46 | } 47 | } else { 48 | return res.status(400).json({ register: 'MISSING FEILD' }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /components/login-btn.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useSession, signIn, signOut } from 'next-auth/react'; 3 | 4 | //new client side login button that can be used anywhere to login for auth and toggles between sign in / sign out 5 | 6 | export default function LoginButton() { 7 | const { data: session }: any = useSession(); 8 | 9 | // if the session exists render the personalized greeting and logout button 10 | if (session) { 11 | return ( 12 | <> 13 |

Welcome, {session.user?.firstName}

14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | // if the session does NOT exist, render the signin button 26 | return ( 27 | <> 28 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css?family=Poppins:100,200,300,400,500,600,700,800,900'); 6 | 7 | .body { 8 | background-image: url('../public/backgroundImg.png'); 9 | background-repeat: no-repeat; 10 | backdrop-filter: blur(100px); 11 | } 12 | 13 | .full-width { 14 | width: 100%; 15 | } 16 | .error { 17 | color: red; 18 | } 19 | .div1 { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | font-family: 'Poppins', sans-serif; 24 | } 25 | 26 | .div2 { 27 | display: flex; 28 | min-height: 8vh; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .content { 34 | position: relative; 35 | } 36 | 37 | .content h2 { 38 | color: #fff; 39 | font-size: 3em; 40 | position: absolute; 41 | transform: translate(-50%, -50%); 42 | } 43 | 44 | .content h2:nth-child(1) { 45 | color: transparent; 46 | -webkit-text-stroke: 2px #0ea5e9; 47 | } 48 | 49 | .content h2:nth-child(2) { 50 | color: #03a9f4; 51 | animation: animate 3s ease-in-out infinite; 52 | } 53 | 54 | @keyframes animate { 55 | 0%, 56 | 100% { 57 | clip-path: polygon( 58 | 0% 45%, 59 | 16% 44%, 60 | 33% 50%, 61 | 54% 60%, 62 | 70% 61%, 63 | 84% 59%, 64 | 100% 52%, 65 | 100% 100%, 66 | 0% 100% 67 | ); 68 | } 69 | 70 | 50% { 71 | clip-path: polygon( 72 | 0% 60%, 73 | 15% 65%, 74 | 34% 66%, 75 | 51% 62%, 76 | 67% 50%, 77 | 84% 45%, 78 | 100% 46%, 79 | 100% 100%, 80 | 0% 100% 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pages/register.tsx: -------------------------------------------------------------------------------- 1 | // IN USE 2 | 3 | import Head from 'next/head'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import RegisterForm from '../components/register'; 7 | import Logo from '../public/LOGO-FINAL.svg' 8 | 9 | // MAIN REGISTER PAGE 10 | export default function Register() { 11 | return ( 12 |
13 | 14 | 15 | Sign Up 16 | 17 | 18 | 19 | 20 |
21 | 22 | {/* SPINNING LOGO */} 23 | Logo 30 | 31 | {/* FANCY POSEIDON TEXT */} 32 |
33 |
34 |
35 |
36 |

POSEIDON

37 |

POSEIDON

38 |
39 |
40 |
41 |
42 | 43 | {/* REGISTER FORM COMPONENT */} 44 |
45 | 46 |
47 | 48 | {/* HAVE AN ACCOUNT BLOCK */} 49 |
50 |

Have an account?

51 | 52 |

Login

53 | 54 |
55 |
56 | 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { NextAuthOptions } from 'next-auth'; 2 | import CredentialsProvider from 'next-auth/providers/credentials'; 3 | import getConfig from 'next/config'; 4 | 5 | const { serverRuntimeConfig } = getConfig(); 6 | 7 | export const authOptions: NextAuthOptions = { 8 | // sets the next option as JWT 9 | session: { 10 | strategy: 'jwt', 11 | }, 12 | // sets the provider as credentials : ie: username / password 13 | providers: [ 14 | CredentialsProvider({ 15 | name: 'credentials', 16 | 17 | credentials: { 18 | email: { label: 'Email', type: 'text', placeholder: 'Email' }, 19 | password: { label: 'Password', type: 'password' }, 20 | }, 21 | 22 | // this is related to the sign in page and sign in function 23 | async authorize(credentials, req) { 24 | const { email, password } = credentials as { 25 | email: string; 26 | password: string; 27 | }; 28 | // perform your login logic - call the api - user - login 29 | // have to do a full url 30 | const res = await fetch(`http://localhost:3500/api/user/login`, { 31 | method: 'POST', 32 | headers: { 33 | Accept: 'application.json', 34 | 'Content-Type': 'application/json', 35 | }, 36 | body: JSON.stringify({ email: email, password: password }), 37 | }); 38 | 39 | const { user } = await res.json(); 40 | 41 | if (!res.ok) { 42 | throw new Error(user.message); 43 | } 44 | if (!user) { 45 | return null; 46 | } 47 | return user; 48 | }, 49 | }), 50 | ], 51 | secret: serverRuntimeConfig.NEXTAUTH_SECRET!, 52 | 53 | pages: { 54 | signIn: '/auth/signin', 55 | }, 56 | 57 | callbacks: { 58 | // the why is below in these articles for syntax 59 | // https://cloudcoders.xyz/blog/nextauth-credentials-provider-with-external-api-and-login-page/ 60 | // https://stackoverflow.com/questions/64576733/where-and-how-to-change-session-user-object-after-signing-in 61 | 62 | async jwt({ token, user }: any) { 63 | user && (token.user = user); 64 | return token; 65 | }, 66 | async session({ session, token }: any) { 67 | const { email, lastName, firstName } = token.user; 68 | session.user = { 69 | email, 70 | lastName, 71 | firstName, 72 | }; 73 | return session; 74 | }, 75 | }, 76 | }; 77 | 78 | export default NextAuth(authOptions); 79 | -------------------------------------------------------------------------------- /copy.md: -------------------------------------------------------------------------------- 1 | [helper resource](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) 2 | 3 | ### Setup 4 | 1. Install [the Docker desktop app](https://www.docker.com/products/docker-desktop/). 5 | 2. In your terminal, `brew install minikube`. 6 | - `kubectl` should display [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) menu. 7 | - See `kubectl --help` for usage. Find more information [here](https://kubernetes.io/docs/reference/kubectl/). 8 | - `minikube` should display minikube menu. 9 | 3. Open your desktop Docker app. 10 | 1. Click on the settings icon in the top right. 11 | 2. Select "Kubernetes" on the left. 12 | 3. Check the box for “Enable Kubernetes”. 13 | 4. Click "Apply and Restart". [Ex](/public/setupExs/apply-restart.png). 14 | 5. Return to the Docker dashboard by clicking on the X icon in the upper right corner. 15 | 4. In your terminal, `minikube start -p [custom cluster name here]`. 16 | - A container for the cluster you labeled should appear.. [Ex](/public/setupExs/minikube-start.jpg). 17 | - The dashboard in your Docker app should show [this](/public/setupExs/containers.png). 18 | - `kubectl get nodes` displays created node. [Ex](/public/setupExs/kubectl-get-nodes.png). 19 | - Note: the "ROLES" column could say "master" instead of “control-plane”. 20 | - `kubectl version` shows your client and server version. [Ex](/public/setupExs/kubectl-version.png). 21 | - `kubectl get pods` currently shouldn't have anything. [Ex](/public/setupExs/kubectl-get-pods.png). 22 | - `kubectl get services` should have one entry. [Ex](/public/setupExs/kubectl-get-services.png). 23 | 5. Navigate to your desired directory. 24 | 1. `brew install prometheus` installs Prometheus. [Ex](/public/setupExs/brew-install-prometheus.png). 25 | 2. `brew install helm` installs Helm. [Ex](/public/setupExs/brew-install-helm.png). 26 | 3. `helm repo add prometheus-community https://prometheus-community.github.io/helm-charts` adds [the Helm repository](https://prometheus-community.github.io/helm-charts/). [Ex](/public/setupExs/helm-repo-add.png). 27 | - Then `helm repo update`. [Ex](/public/setupExs/helm-repo-update.png). 28 | 4. `helm install [named cluster here] prometheus-community/kube-prometheus-stack`. [Ex](/public/setupExs/helm-install-cluster.png). 29 | - Visit [here](https://github.com/prometheus-operator/kube-prometheus) for instructions on how to create & configure Alertmanager and Prometheus instances using the Operator. 30 | 5. `kubectl get pod` displays all the pods. [Ex](/public/setupExs/kubectl-get-pod.png). 31 | 1. From the results of the above command, copy the last "NAME" entry except for the starting "prometheus-" and trailling "-0" part. 32 | 2. Then `kubectl port-forward svc/[paste it here] 9090`. [Ex](/public/setupExs/kubectl-port-forward.png). 33 | 3. Visit http://localhost:9090 in the browser, to see the Prometheus web interface. [Ex](/public/setupExs/visit-9090.png). 34 | - Select "Status" from the above navigation bar then "Targets", to see a list of pre-configured scrape targets. [Ex](/public/setupExs/status-targets.png). 35 | 6. ...TO BE CONTINUED. 36 | 37 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 | {/* CALL TO ACTION, PLEASE VISIT OUR OSP SITE */} 7 |
8 |
9 |

10 | Learn More 11 | 12 | {' '} 13 | About 14 | 15 | {' '} 16 | Poseidon 17 | {' '} 18 | 19 |

20 |
21 | 26 |
27 | 28 | Main Page 29 | 30 | 31 |
32 |
33 |
34 | {/* 35 |
*/} 36 | 37 | {/* LINKS TO EACH INDIVIDUAL LINKEDIN PAGE */} 38 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | any visit to http://localhost:3500/ should check cookies 4 | if no cookies immediate direct to /auth/signin 5 | else displays dashboard 6 | */ 7 | describe('login page', () => { 8 | beforeEach(() => { cy.visit('localhost:3500/auth/signin'); }); 9 | // it('logo is displayed', () => {}); 10 | it('POSEIDON is displayed', () => { cy.contains('h2', 'POSEIDON'); }); 11 | context('form inputs', () => { 12 | beforeEach(() => { cy.get('form'); }); 13 | it('requires email', () => { 14 | cy.contains('Sign In').click() 15 | .get('div').should('contain', 'Please enter your email'); 16 | }); 17 | it('requires password', () => { 18 | cy.contains('Sign In').click() 19 | .get('div').should('contain', 'Please enter your password'); 20 | }); 21 | it('entry into dashboard', () => { 22 | cy.get('input[name="email"]').type('josh@email.com') 23 | .get('input[name="password"]').type('password') 24 | .get('button').contains('Sign In').click() 25 | // .log('CHECK INCOMPLETE NEED REGEX URL'); 26 | // .url().should('match', 'localhost:3500') 27 | .url().should('include', '/'); 28 | // // UI should reflect this user being logged in 29 | // cy.get('h1').should('contain', 'jane.lane') 30 | }); 31 | }); 32 | 33 | // // it('Need an account?', () => { 34 | // // cy.contains('Need an account?').should('have.attr', 'href', '#/register'); 35 | // // }); 36 | it('Sign Up button links to /register', () => { 37 | cy.contains('Sign Up').should('have.attr', 'href', '/register'); 38 | }); 39 | 40 | // it('Upon 3500, checks cookies', () => { 41 | // cy.visit('http://localhost:3500/'); 42 | // cy.url().should('include', '/auth/signin'); 43 | // }); 44 | }); 45 | // console.log(see); 46 | 47 | 48 | // BELOW IS HOW AUTH SHOULD LOOK LIKE 49 | // describe('The Login Page', () => { 50 | // beforeEach(() => { 51 | // // reset and seed the database prior to every test 52 | // cy.exec('npm run db:reset && npm run db:seed') 53 | 54 | // // seed a user in the DB that we can control from our tests 55 | // // assuming it generates a random password for us 56 | // cy.request('POST', '/test/seed/user', { username: 'jane.lane' }) 57 | // .its('body') 58 | // .as('currentUser') 59 | // }) 60 | 61 | // it('sets auth cookie when logging in via form submission', function () { 62 | // // destructuring assignment of the this.currentUser object 63 | // const { username, password } = this.currentUser 64 | 65 | // cy.visit('/login') 66 | 67 | // cy.get('input[name=username]').type(username) 68 | // // {enter} causes the form to submit 69 | // cy.get('input[name=password]').type(`${password}{enter}`) 70 | // // we should be redirected to /dashboard 71 | // cy.url().should('include', '/dashboard') 72 | // // our auth cookie should be present 73 | // cy.getCookie('your-session-cookie').should('exist') 74 | // }) 75 | // }) 76 | 77 | 78 | describe('register', () => { 79 | 80 | it('no fields may be empty', () => { 81 | cy.visit('localhost:3500/register') 82 | .get('input[name="firstName"]').should('have.value', '') 83 | .get('input[name="lastName"]').should('have.value', '') 84 | .get('input[name="email"]').should('have.value', '') 85 | .get('input[name="password"]').should('have.value', '') 86 | .get('input[name="changePassword"]').should('have.value', '') 87 | .get('form').contains('Register').click() 88 | .url().should('include', '/register'); 89 | }); 90 | // it('lastname can\'t be empty', () => {}); 91 | // it('email can\'t be empty', () => {}); 92 | 93 | }); -------------------------------------------------------------------------------- /constants.ts: -------------------------------------------------------------------------------- 1 | import getConfig from 'next/config'; 2 | const { publicRuntimeConfig } = getConfig(); 3 | 4 | export const name_local: string = publicRuntimeConfig.LOCAL_CLUSTER_NAME; 5 | export const name_deployed: string = publicRuntimeConfig.DEPLOYED_CLUSTER_NAME; 6 | export const name_deployed_2: string = 7 | publicRuntimeConfig.DEPLOYED_CLUSTER_NAME_2; 8 | 9 | const IP_local_env: string = publicRuntimeConfig.LOCAL_CLUSTER_IP; 10 | const IP_deployed_env: string = publicRuntimeConfig.DEPLOYED_CLUSTER_IP; 11 | 12 | export const dashUrls: string[][] = [ 13 | // ARRAY FOR DEPLOYED CLUSTER 14 | [ 15 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=2`, 16 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=3`, 17 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=4`, 18 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=5`, 19 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=6`, 20 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=10`, 21 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=8`, 22 | `http://${IP_deployed_env}/d-solo/${name_deployed_2}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=9`, 23 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=2`, 24 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=3`, 25 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=4`, 26 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=5`, 27 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=6`, 28 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=7`, 29 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=8`, 30 | `http://${IP_deployed_env}/d-solo/${name_deployed}/node-exporter-nodes?orgId=1&refresh=30s&from=now-2h&to=now&panelId=9`, 31 | ], 32 | 33 | // ARRAY FOR LOCAL CLUSTER (MINIKUBE), grafana iframe embed links example 34 | [ 35 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=30s&from=now-24h&to=now&panelId=2`, 36 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=30s&from=now-24h&to=now&panelId=3`, 37 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=4`, 38 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=5`, 39 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=6`, 40 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=7`, 41 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=8`, 42 | `http://${IP_local_env}/d-solo/${name_local}/node-exporter-nodes?orgId=1&refresh=1d&from=now-24h&to=now&panelId=9`, 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /components/register.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Formik, Field, Form, FormikHelpers } from 'formik'; 3 | import { useRouter } from 'next/navigation'; 4 | import * as Yup from "yup"; 5 | import { Input } from './input'; 6 | 7 | export default function RegisterForm() { 8 | const router = useRouter(); 9 | 10 | const loginUser = async (body: RegisterValues) => { 11 | try { 12 | const res = await fetch(`/api/user/register`, { 13 | method: 'POST', 14 | headers: { 15 | Accept: 'application.json', 16 | 'Content-Type': 'application/json', 17 | }, 18 | body: JSON.stringify(body), 19 | }); 20 | // if data present - redirect to dashboard 21 | const { user } = await res.json(); 22 | // if data present - redirect to dashboard 23 | if (user) { 24 | // may need to route to login to confirm they know their information 25 | router.push(`/`); 26 | } else { 27 | // user is not in the db - route to sign up page 28 | // NEED TO ADD MESSAGE EXPLAINING WHAT TO DO + WHY 29 | router.push(`/login`); 30 | } 31 | } catch (err) { 32 | console.log(err); 33 | } 34 | }; 35 | 36 | return ( 37 |
38 | 39 |
40 |
41 |

42 | Register 43 |

44 | (val && val.length > 0 ? true : false), 65 | then: Yup.string().oneOf( 66 | [Yup.ref("password")], 67 | "Both password need to be the same" 68 | ) 69 | }), 70 | lastName: Yup.string() 71 | .required("Required") 72 | .max(30, 'Must be 30 characters or less'), 73 | firstName: Yup.string() 74 | .required("Required") 75 | .max(30, 'Must be 30 characters or less'), 76 | ipAddress: Yup.string() 77 | .required("Required") 78 | .max(30, 'Must be 30 characters or less'), 79 | })} 80 | 81 | // unsure about this error 82 | onSubmit={( 83 | values: RegisterValues, 84 | { setSubmitting }: FormikHelpers 85 | ) => { 86 | loginUser({ 87 | email: values.email, 88 | password: values.password, 89 | firstName: values.firstName, 90 | lastName: values.lastName, 91 | ipAddress: values.ipAddress, 92 | }); 93 | setSubmitting(false); 94 | }} 95 | > 96 |
97 | {/* first name input */} 98 |
99 | 105 |
106 | 107 | {/* last name input */} 108 |
109 | 115 |
116 | 117 | {/* email input */} 118 |
119 | 125 |
126 | 127 | {/* ip address input */} 128 |
129 | 134 |
135 | 136 | {/* password input */} 137 |
138 | 144 |
145 | 146 |
147 | 153 |
154 | 155 | 156 | 157 | {/* MAY WANT TO HAVE FIELD TO CONFIRM PASSWORD and give feedback on the info marching up */} 158 |
159 | 166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | ); 174 | } 175 | -------------------------------------------------------------------------------- /components/dashboard/dashboardContainer.tsx: -------------------------------------------------------------------------------- 1 | import { dashUrls } from '../../constants'; 2 | import CostComponent from './costComponent'; 3 | import Grafana from './grafana'; 4 | import { useState, useEffect } from 'react'; 5 | import useSWR from 'swr'; 6 | import CostError from './costError'; 7 | import Spinner from './spinner'; 8 | import getConfig from 'next/config'; 9 | const { publicRuntimeConfig } = getConfig(); 10 | 11 | const fetcher = async (url: string) => fetch(url).then((res) => res.json()); 12 | export default function DashboardContainer() { 13 | const [clusterType, setClusterType] = useState('deployed'); 14 | const kubeip = publicRuntimeConfig.KUBECOST_IP; 15 | const localKubeip = publicRuntimeConfig.KUBECOST_IP; 16 | const localClusterIp = publicRuntimeConfig.LOCAL_CLUSTER_IP; 17 | const deployedClusterIp = publicRuntimeConfig.DEPLOYED_CLUSTER_IP; 18 | 19 | const { data, error, isLoading } = useSWR( 20 | `http://${kubeip}:9090/model/allocation?window=15d&aggregate=cluster`, 21 | fetcher 22 | ); 23 | let deployedVisualizers; 24 | // array for deployed clusters 25 | if (dashUrls[0][0]) { 26 | deployedVisualizers = dashUrls[0][0] 27 | ? dashUrls[0].map((url, i) => ) 28 | : [ 29 |
30 | You need to connect deployed clusters to see this data! 31 |
, 32 | ]; 33 | } 34 | 35 | // array for local clusters 36 | const localVisualizers = dashUrls[1][0] 37 | ? dashUrls[1].map((url, i) => ) 38 | : [ 39 |
43 | You need to connect local clusters to see this data! 44 |
, 45 | ]; 46 | 47 | const toggleSelection = (e: React.SyntheticEvent) => { 48 | e.preventDefault(); 49 | setClusterType(e.currentTarget.id); 50 | }; 51 | 52 | useEffect(() => { 53 | let clusterIndex = clusterType === 'deployed' ? 0 : 1; 54 | }, [clusterType]); 55 | 56 | return ( 57 |
58 |
59 |
60 | 99 |
100 |
101 | {/* insert both dataURL[0] and dataURL[1] with either grafana tags or the div*/} 102 |
109 | {/*@ts-ignore*/} 110 | {deployedClusterIp ? ( 111 | deployedVisualizers 112 | ) : ( 113 | <> 114 |
115 | You need to add deployed cluster IP address to .env.local file 116 | to see this data! 117 |
118 | 119 | )} 120 |
121 |
128 | {localClusterIp ? ( 129 | <>{localVisualizers} 130 | ) : ( 131 | <> 132 |
133 | You need to add local cluster IP address to .env.local file to 134 | see this data! 135 |
136 | 137 | )} 138 |
139 |
146 | 147 |
148 |
149 |
150 |
151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | ## [![banner](/public/assets/banner.png)](https://www.os-poseidon.com/) 5 | 6 | 7 | [![Next JS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white)](https://nextjs.org/) 8 | [![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)](https://reactjs.org/) 9 | [![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 10 | [![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white)](https://grafana.com/) 11 | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://kubernetes.io/) 12 | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) 13 | [![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white)](https://prometheus.io/) 14 | [![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white)](https://tailwindcss.com/) 15 | [![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white)](https://www.mongodb.com/) 16 | [![Cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e)](https://www.cypress.io/) 17 | [![License](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](public/LICENSE) 18 | 19 | ### This Cluster Data Visualizer and Cost Analysis app delivers cost estimates, stunning visualizations, and crucial insights to optimize deployments and stay within budget. 20 | 21 | 22 | 23 | 24 | 25 | [![Medium](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@drjoshdpt/poseidon-a-kubernetes-cluster-visualization-cost-analysis-tool-d0fb55c2858c) 26 | [![YouTube](https://img.shields.io/badge/YouTube-%23FF0000.svg?style=for-the-badge&logo=YouTube&logoColor=white)](https://youtu.be/hqeGqh0ksPg) 27 | [![LinkedIn](https://img.shields.io/badge/linkedin-%230077B5.svg?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/osposeidon/) 28 |
29 | 30 |
31 |

Quick Start

32 | 33 | 1. Fork this repository to your profile, clone it to your local machine, navigate into the directory, then run `npm install`. 34 | 2. Ensure your local and/or cloud clusters are deployed, then within the root directory, create a `.env.local` file and paste in the following: 35 | ``` 36 | NODE_ENV = development 37 | MONGO_URI = "[REPLACE THIS WITH YOUR MONGODB CONNECTION STRING]" 38 | NEXTAUTH_URL = http://localhost:3500 39 | BCRYPT_SALT = 10 40 | NEXTAUTH_SECRET = 12345678910 41 | KUBECOST_IP = "[REPLACE THIS WITH YOUR DEPLOYED KUBECOST IP ADDRESS]" 42 | LOCAL_KUBECOST_IP = "[REPLACE THIS WITH YOUR LOCAL KUBECOST IP ADDRESS]" 43 | LOCAL_CLUSTER_IP = "localhost:3300" 44 | LOCAL_CLUSTER_NAME = "[REPLACE THIS WITH THE UNIQUE EMBED ID FROM YOUR LOCAL CLUSTER'S GRAFANA SHARE LINK]" 45 | DEPLOYED_CLUSTER_IP = "[REPLACE THIS WITH YOUR DEPLOYED* CLUSTER IP ADDRESS]" 46 | DEPLOYED_CLUSTER_NAME = "[REPLACE THIS WITH THE UNIQUE EMBED ID FROM YOUR CLOUD HOSTED GRAFANA SHARE LINK]" 47 | DEPLOYED_CLUSTER_NAME_2 = "[OPTIONAL: REPLACE THIS WITH THE UNIQUE EMBED ID FROM YOUR CLOUD HOSTED GRAFANA SHARE LINK]" 48 | ``` 49 | - Be sure to replace the areas of all caps text including the enclosing square brackets with the specified information, then save the file. 50 | - *Functions for both depolyments on AWS and/or Google. 51 | - For more detailed instructions, please visit our website [os-poseidon.com/setup](https://www.os-poseidon.com/setup). 52 | 3. Regarding your command options: 53 | - `npm run port` runs the application on port 3500 specifically for cloud deployments. 54 | - `npm run forward` runs the application on port 3500 for both local and cloud deployments. 55 | - `npm run test` assures that any processes on port 3500 is killed then in parallel runs the application on that port and opens Cypress. 56 |
57 | 58 | 59 | ## Current Features 60 | - Cloud and local K8s cluster support 61 | - Cluster metric visualization 62 | - Cost-analysis via Kubecost 63 | 64 | ## Iteration Plans 65 | - [ ] Apply a regex functionality to identify the id name from embed urls and provide an input field in the registration form to store cluster IP addresses for the client's ease of use. 66 | - [ ] Connect a feature to directly customize cluster specifics 67 | - [ ] Implement an authentication hierarchy to allow for role and privilege assignments. This secures and regulates any manipulations to an organization's clusters. 68 | - [ ] Create a filter for the graphs on the dashboard for cluster and data types. 69 | - [ ] Integrate D3.js for graph styling. 70 | - [ ] Move forward with AWS hosting for online deployment of the app which allows for greater scalability and dependability. 71 | - [ ] Develop a notification system within the application to enable users to receive notifications, such as through the Slack API, in case their clusters experiences downtime. 72 | - [ ] Improve visual comfort and reduce eye strain by implementing dark mode and including dynamic display scaling. 73 | 74 | 75 | 76 | 86 | 87 | ## Connect with the Team! 88 | | Joshuah Edwards | Sarah Chow | Alina Grafkina | Will Moody | 89 | | :---: | :---: | :---: | :---: | 90 | | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/JoshDPT) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/joshuah-edwards/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/xtchow) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/xtchow/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/alyagraf) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/alina-grafkina-955a56179/) | [![GitHub](https://skillicons.dev/icons?i=github)](https://github.com/wmoody6293) [![LinkedIn](https://skillicons.dev/icons?i=linkedin)](https://www.linkedin.com/in/william-moody/) | 91 | 92 | 93 | -------------------------------------------------------------------------------- /pages/auth/signin.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { signIn, getCsrfToken } from 'next-auth/react'; 3 | import { Formik, Field, ErrorMessage } from 'formik'; 4 | import * as Yup from 'yup'; 5 | import { useRouter } from 'next/router'; 6 | import Head from 'next/head'; 7 | import Image from 'next/image'; 8 | import Logo from '../../public/LOGO-FINAL.svg'; 9 | export default function SignIn({ csrfToken }: any): JSX.Element { 10 | const router = useRouter(); 11 | const [error, setError] = useState(null); 12 | 13 | return ( 14 | <> 15 | 16 | Sign In 17 | 18 | 19 | 20 | { 36 | const res: any = await signIn('credentials', { 37 | redirect: false, 38 | email: values.email, 39 | password: values.password, 40 | callbackUrl: `${window.location.origin}`, 41 | }); 42 | if (res?.error) { 43 | setError(res.error); 44 | } else { 45 | setError(null); 46 | } 47 | if (res.url) router.push(res.url); 48 | setSubmitting(false); 49 | }} 50 | > 51 | {(formik) => ( 52 |
53 |
54 |
55 | Logo 62 | 63 |
64 |
65 |
66 |
67 |

POSEIDON

68 |

POSEIDON

69 |
70 |
71 |
72 |
73 | 74 | 79 | 80 |
81 | {error} 82 |
83 | 84 |
85 | 100 | 101 |
102 | 103 |
104 |
105 |
106 | 121 | 122 |
123 | 124 |
125 |
126 | 127 |
128 | 134 |
135 | 136 | {/* SWITCH TO SIGN UP PAGE */} 137 |
138 | 139 |

140 | {' '} 141 | Don't have an account?{' '} 142 |

143 |
144 | 148 |

Sign Up

149 | 150 |
151 |
152 |
153 |
154 |
155 | )} 156 |
157 | 158 | ); 159 | } 160 | 161 | // This is the recommended way for Next.js 9.3 or newer 162 | export async function getServerSideProps(context: any) { 163 | return { 164 | props: { 165 | csrfToken: await getCsrfToken(context), 166 | }, 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /components/dashboard/costComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import useSWR from 'swr'; 3 | import getConfig from 'next/config'; 4 | import Spinner from './spinner'; 5 | const { publicRuntimeConfig } = getConfig(); 6 | const fetcher = async (url: string) => fetch(url).then((res) => res.json()); 7 | 8 | export default function CostComponent({ deployedCost }: any) { 9 | let refinedData: any = ''; 10 | let tableVals: any = ''; 11 | let dataArr: any; 12 | if (deployedCost) { 13 | dataArr = deployedCost.data; 14 | } 15 | const kubeip = publicRuntimeConfig.LOCAL_KUBECOST_IP; 16 | const { data, error, isLoading } = useSWR( 17 | `http://${kubeip}:9090/model/allocation?window=15d&aggregate=cluster`, 18 | fetcher 19 | ); 20 | const [kubeCostVals, setKubeCostVals] = useState({}); 21 | const [clusterType, setClusterType] = useState('deployed'); 22 | const [localClusters, setLocalClusters] = useState([]); 23 | useEffect(() => { 24 | const sortFetchedData = async (parsedArr: any) => { 25 | const getProps = async (parsedArr: []) => { 26 | let totalCPU = 0; 27 | let totalRAM = 0; 28 | let totalGPU = 0; 29 | let totalloadBalancerCost = 0; 30 | let totalNetworkCost = 0; 31 | let totalPVCost = 0; 32 | let totalCost = 0; 33 | let totalSharedCost = 0; 34 | let totalDays = 0; 35 | 36 | // add cost efficiency 37 | function getMetrics(metricObject: any) { 38 | for (const key in metricObject) { 39 | if (key === 'cpuCost') totalCPU += metricObject[key]; 40 | if (key === 'gpuCost') totalGPU += metricObject[key]; 41 | if (key === 'ramCost') totalRAM += metricObject[key]; 42 | if (key === 'networkCost') totalNetworkCost += metricObject[key]; 43 | if (key === 'loadBalancerCost') 44 | totalloadBalancerCost += metricObject[key]; 45 | if (key === 'pvCost') totalPVCost += metricObject[key]; 46 | if (key === 'totalCost') totalCost += metricObject[key]; 47 | if (key === 'sharedCost') totalSharedCost += metricObject[key]; 48 | } 49 | } 50 | parsedArr.forEach((obj: any) => { 51 | //this will ignore any objects that are empty (if cluster only has a few days worth of data) 52 | if (obj.__idle__) { 53 | totalDays += 1; 54 | for (const key in obj) { 55 | getMetrics(obj[key]); 56 | } 57 | } 58 | }); 59 | const data = { 60 | 'CPU Cost': totalCPU, 61 | 'RAM Cost': totalRAM, 62 | 'GPU Cost': totalGPU, 63 | 'Load Balancer Cost': totalloadBalancerCost, 64 | 'Network Cost': totalNetworkCost, 65 | 'PV Cost': totalPVCost, 66 | 'Shared Cost': totalSharedCost, 67 | 'Total Cost': totalCost, 68 | totalDays, 69 | }; 70 | setKubeCostVals(data); 71 | }; 72 | getProps(parsedArr); 73 | }; 74 | if (clusterType === 'deployed' && deployedCost) { 75 | sortFetchedData(dataArr); 76 | } else if (clusterType === 'local' && !error) { 77 | console.log('data sortFetchedData: ', data); 78 | sortFetchedData(data.data); 79 | } 80 | }, [setKubeCostVals, dataArr, clusterType, data, getAverages, makeTableVals]); 81 | 82 | function makeTableVals(data: any): void { 83 | if (data) { 84 | const newTableVals = []; 85 | for (const key in data) { 86 | if (data[key] !== 0 && key !== 'totalDays') { 87 | newTableVals.push( 88 | 92 | 96 | {key === 'Total Cost' ? ( 97 |

{key}:

98 | ) : ( 99 | key 100 | )} 101 | 102 | 109 | ${data[key].toFixed(2)} 110 | 111 | 112 | ); 113 | } 114 | } 115 | tableVals = newTableVals; 116 | } 117 | } 118 | function getAverages(data: any) { 119 | if (data) { 120 | //take totalDays 121 | const totalDays = data.totalDays; 122 | //create new set of variables to store updated projected costs for each val in data 123 | const averageCost: any = {}; 124 | 125 | //iterate over the rest of data 126 | for (const key in data) { 127 | //calc the average cost/day/component 128 | averageCost[key] = (data[key] / totalDays) * 30; 129 | } 130 | refinedData = averageCost; 131 | return; 132 | } 133 | } 134 | 135 | const toggleSelection = (e: React.SyntheticEvent) => { 136 | e.preventDefault(); 137 | setClusterType(e.currentTarget.id); 138 | }; 139 | if (clusterType === 'deployed' && deployedCost !== undefined) { 140 | getAverages(kubeCostVals); 141 | makeTableVals(refinedData); 142 | } else if (clusterType === 'local' && !error) { 143 | getAverages(kubeCostVals); 144 | makeTableVals(refinedData); 145 | } 146 | return ( 147 |
148 |
149 | 150 | 151 | 152 | 156 | 160 | 161 | 162 | 163 | {tableVals ? ( 164 | tableVals 165 | ) : ( 166 | 167 | 176 | 182 | 183 | )} 184 | 185 |
153 | Cost
154 | Category 155 |
157 | Total Cost
158 | per month 159 |
171 |

172 | You Need To Connect To Kubecost 173 |
Before You Can See Expected Cluster Costs 174 |

175 |
177 |

178 | You Need To Connect To Kubecost 179 |
Before You Can See Expected Cluster Costs 180 |

181 |
186 |
187 |
188 |
189 | All cost figures are monthly estimates based on data retrieved from 190 | Kubecost. 191 |
192 |
193 |
194 | 222 |
223 |
224 | ); 225 | } 226 | -------------------------------------------------------------------------------- /public/LOGO-FINAL.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 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 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | --------------------------------------------------------------------------------