From f2f626f367451d93a08b6fc024457cb7fe770b15 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 21 Mar 2022 02:46:24 -0700 Subject: [PATCH] restat in cli and fix ws --- cli/src/cli.tsx | 5 + cli/src/components/Restart.tsx | 164 ++++++++++++++++++ cli/src/components/util/EnterToContinue.tsx | 5 +- cli/src/components/util/LabelledSelector.tsx | 4 +- cli/src/components/util/Selector.tsx | 3 + cli/src/components/util/YesNo.tsx | 5 +- cli/src/util/helpers/deploy.ts | 1 + cli/src/util/helpers/docker.ts | 83 ++++++++- cli/src/util/helpers/restart.ts | 37 ++++ cli/src/util/mongoose/deployment.ts | 18 +- core/src/plugins/ws.ts | 15 +- frontend/src/components/UserInfo.tsx | 17 +- .../deployment/Deployment.module.css | 3 + .../src/components/deployment/Deployment.tsx | 14 ++ .../src/components/deployment/tabs/Config.tsx | 13 ++ .../components/deployment/tabs/ErrorLog.tsx | 11 ++ .../src/components/deployment/tabs/Log.tsx | 11 ++ frontend/src/index.tsx | 2 +- frontend/src/state/UserProvider.tsx | 6 +- util/docker.ts | 15 +- 20 files changed, 400 insertions(+), 32 deletions(-) create mode 100644 cli/src/components/Restart.tsx create mode 100644 cli/src/util/helpers/restart.ts create mode 100644 frontend/src/components/deployment/Deployment.module.css create mode 100644 frontend/src/components/deployment/Deployment.tsx create mode 100644 frontend/src/components/deployment/tabs/Config.tsx create mode 100644 frontend/src/components/deployment/tabs/ErrorLog.tsx create mode 100644 frontend/src/components/deployment/tabs/Log.tsx diff --git a/cli/src/cli.tsx b/cli/src/cli.tsx index 311acfee6..b6bd14e1a 100644 --- a/cli/src/cli.tsx +++ b/cli/src/cli.tsx @@ -12,6 +12,7 @@ import Registry from "./components/deployment-config/Registry"; import CoreOrPeriphery from "./components/core-or-periphery/CoreOrPeriphery"; import { bound } from "./util/helpers/general"; import Setup from "./components/Setup"; +import Restart from "./components/Restart"; type Page = { title: string; @@ -61,6 +62,10 @@ init().then(({ flags, dockerInstalled }) => { title: "docker intro", view: , }, + { + title: "restart", + view: , + }, !flags.core && !flags.periphery ? { title: "core or periphery", diff --git a/cli/src/components/Restart.tsx b/cli/src/components/Restart.tsx new file mode 100644 index 000000000..b76d1781e --- /dev/null +++ b/cli/src/components/Restart.tsx @@ -0,0 +1,164 @@ +import React, { Fragment, useEffect } from "react"; +import LabelledSelector from "./util/LabelledSelector"; +import { useMainSequence } from "../cli"; +import { useStore } from "../util/hooks"; +import { Box, Newline, Text } from "ink"; +import { Input } from "./util/Input"; +import EnterToContinue from "./util/EnterToContinue"; +import { CommandLogError } from "@monitor/types"; +import { restart, RestartError } from "../util/helpers/restart"; + +type State = { + stage: + | "query" + | "name" + | "mongo" + | "confirm" + | "installing" + | "finished" + | "error"; + name: string; + mongoUrl?: string; + result?: CommandLogError; + error?: RestartError; +}; + +const Restart = () => { + const { next, prev } = useMainSequence(); + const [config, setConfig, setMany] = useStore({ + stage: "query", + name: "monitor-core", + }); + + const { stage, name, mongoUrl, result, error } = config; + + useEffect(() => { + if (stage === "installing") { + restart({ name, mongoUrl: mongoUrl! }, (err) => + setMany(["stage", "error"], ["error", err]) + ).then((success) => { + if (success) { + setMany(["stage", "finished"], ["result", success]); + } + }); + } else if (stage === "finished" || stage === "error") { + process.exit(); + } + }, [stage]); + + if (stage === "query") { + return ( + { + switch (option) { + case "deploy monitor core": + next(); + break; + + case "restart monitor core": + setConfig("stage", "name"); + break; + } + }} + onEsc={prev} + vertical + /> + ); + } else { + return ( + + + name:{" "} + + {stage === "name" ? ( + setMany(["stage", "mongo"], ["name", name])} + onEsc={() => setConfig("stage", "query")} + /> + ) : ( + name + )} + + + + {stage === "mongo" && ( + + mongo url:{" "} + + + setMany(["stage", "confirm"], ["mongoUrl", mongoUrl]) + } + onEsc={() => setConfig("stage", "name")} + /> + + + )} + + {mongoUrl && stage !== "mongo" && ( + + mongo url: {mongoUrl} + + )} + + + + {stage === "confirm" && ( + { + setConfig("stage", "installing"); + }} + onEsc={() => setConfig("stage", "mongo")} + pressEnterTo="restart monitor" + /> + )} + + {(stage === "installing" || stage === "error") && ( + + restarting... + + )} + + {result && ( + + finished restarting + + + + command: {result.command} + + {result.log.stderr ? ( + + stderr: {result.log.stderr} + + ) : undefined} + {result.log.stdout ? ( + + stdout: {result.log.stdout} + + ) : undefined} + + + + )} + + {error && ( + + + error restarting + + {error.message} + {error.error} + + + )} + + ); + } +}; + +export default Restart; diff --git a/cli/src/components/util/EnterToContinue.tsx b/cli/src/components/util/EnterToContinue.tsx index 3bebd4912..7c7a1b7d5 100644 --- a/cli/src/components/util/EnterToContinue.tsx +++ b/cli/src/components/util/EnterToContinue.tsx @@ -1,9 +1,10 @@ import React from "react"; import { Text } from "ink"; -import { useEnter } from "../../util/hooks"; +import { useEnter, useEsc } from "../../util/hooks"; -const EnterToContinue = ({ onEnter, pressEnterTo }: { onEnter: () => void; pressEnterTo?: string }) => { +const EnterToContinue = ({ onEnter, pressEnterTo, onEsc }: { onEnter: () => void; pressEnterTo?: string; onEsc?: () => void; }) => { useEnter(onEnter); + useEsc(() => onEsc && onEsc()); return ( press{" "} diff --git a/cli/src/components/util/LabelledSelector.tsx b/cli/src/components/util/LabelledSelector.tsx index 79a09e65d..1a6e6efb3 100644 --- a/cli/src/components/util/LabelledSelector.tsx +++ b/cli/src/components/util/LabelledSelector.tsx @@ -6,6 +6,7 @@ const LabelledSelector = ({ label, items, onSelect, + onEsc, vertical, labelColor = "white", }: { @@ -14,6 +15,7 @@ const LabelledSelector = ({ items: string[]; onSelect?: (item: string, index: number) => void; vertical?: boolean; + onEsc?: () => void; }) => { return ( @@ -23,7 +25,7 @@ const LabelledSelector = ({ label )} {vertical && } - + ); }; diff --git a/cli/src/components/util/Selector.tsx b/cli/src/components/util/Selector.tsx index 819f906f2..59e1370ff 100644 --- a/cli/src/components/util/Selector.tsx +++ b/cli/src/components/util/Selector.tsx @@ -4,6 +4,7 @@ import { Box, Text, useInput } from "ink"; const Selector = (p: { items: string[]; onSelect?: (item: string, i: number) => void; + onEsc?: () => void; }) => { const [highlighted, setHighlighted] = useState(0); useInput((_, key) => { @@ -13,6 +14,8 @@ const Selector = (p: { setHighlighted(Math.min(highlighted + 1, p.items.length - 1)); } else if (key.return) { if (p.onSelect) p.onSelect(p.items[highlighted]!, highlighted); + } else if (key.escape) { + if (p.onEsc) p.onEsc(); } }); return ( diff --git a/cli/src/components/util/YesNo.tsx b/cli/src/components/util/YesNo.tsx index d6cb835ba..10f07ef4a 100644 --- a/cli/src/components/util/YesNo.tsx +++ b/cli/src/components/util/YesNo.tsx @@ -8,7 +8,8 @@ const YesNo = ({ onSelect, vertical, labelColor, - noYes + noYes, + onEsc }: { label: ReactNode; onYes?: () => void; @@ -17,6 +18,7 @@ const YesNo = ({ vertical?: boolean; labelColor?: "green" | "white"; noYes?: boolean; + onEsc?: () => void; }) => { return ( ); }; diff --git a/cli/src/util/helpers/deploy.ts b/cli/src/util/helpers/deploy.ts index e339d9ee1..ea6cb069f 100644 --- a/cli/src/util/helpers/deploy.ts +++ b/cli/src/util/helpers/deploy.ts @@ -107,3 +107,4 @@ async function createNetwork() { const command = `docker network create ${DOCKER_NETWORK}`; return await execute(command); } + diff --git a/cli/src/util/helpers/docker.ts b/cli/src/util/helpers/docker.ts index cbec4b3db..0aac7800e 100644 --- a/cli/src/util/helpers/docker.ts +++ b/cli/src/util/helpers/docker.ts @@ -1,4 +1,4 @@ -import { CommandLogError } from "@monitor/types"; +import { CommandLogError, Conversion, DockerRunArgs, EnvironmentVar, Volume } from "@monitor/types"; import { execute } from "./execute"; export type InstallLog = { @@ -102,3 +102,84 @@ export async function isDockerInstalled() { const res = await execute("docker ps"); return !res.isError; } + +export async function deleteContainer(containerName: string) { + return await execute( + `docker stop ${containerName} && docker container rm ${containerName}` + ); +} + +/* Docker Run for Deployments */ +export async function dockerRun( + { + image, + latest, + ports, + environment, + network, + volumes, + restart, + postImage, + containerName, + containerUser, + }: DockerRunArgs +) { + const command = + `docker run -d` + + name(containerName) + + containerUserString(containerUser) + + portsString(ports) + + volsString(volumes) + + envString(environment) + + restartString(restart) + + networkString(network) + + ` ${image}${latest ? ":latest" : ""}${postImage ? " " + postImage : ""}`; + + return await execute(command); +} + +function name(containerName?: string) { + return containerName ? ` --name ${containerName}` : ""; +} + +function portsString(ports?: Conversion[]) { + return ports && ports.length > 0 + ? ports + .map(({ local, container }) => ` -p ${local}:${container}`) + .reduce((prev, curr) => prev + curr) + : ""; +} + +function volsString(volumes?: Volume[]) { + return volumes && volumes.length > 0 + ? volumes + .map(({ local, container }) => { + return ` -v ${local}:${container}`; + }) + .reduce((prev, curr) => prev + curr) + : ""; +} + +function restartString(restart?: string) { + return restart + ? ` --restart=${restart}${restart === "on-failure" ? ":10" : ""}` + : ""; +} + +function envString(environment?: EnvironmentVar[]) { + return environment && environment.length > 0 + ? environment + .map(({ variable, value }) => ` -e "${variable}=${value}"`) + .reduce((prev, curr) => prev + curr) + : ""; +} + +function networkString(network?: string) { + return network ? ` --network ${network}` : ""; +} + +function containerUserString(containerUser?: string) { + return containerUser && containerUser.length > 0 + ? ` -u ${containerUser}` + : ""; +} \ No newline at end of file diff --git a/cli/src/util/helpers/restart.ts b/cli/src/util/helpers/restart.ts new file mode 100644 index 000000000..e695d3589 --- /dev/null +++ b/cli/src/util/helpers/restart.ts @@ -0,0 +1,37 @@ +import { getCoreDeployment } from "../mongoose/deployment"; +import { deleteContainer, dockerRun } from "./docker"; + +export type RestartError = { + message: string; + error: string; +} + +export async function restart( + args: { name: string; mongoUrl: string }, + onError: (err: RestartError) => void +) { + try { + const deployment = await getCoreDeployment(args); + if (deployment) { + try { + await deleteContainer(deployment.containerName!); + return await dockerRun(deployment); + } catch (error) { + onError({ + message: "failed to restart container", + error: JSON.stringify(error), + }); + } + } else { + onError({ + message: "could not find deployment at name", + error: "", + }); + } + } catch (error) { + onError({ + message: "failed to connect to mongo at url", + error: JSON.stringify(error), + }); + } +} diff --git a/cli/src/util/mongoose/deployment.ts b/cli/src/util/mongoose/deployment.ts index cb8445dcd..7877e8663 100644 --- a/cli/src/util/mongoose/deployment.ts +++ b/cli/src/util/mongoose/deployment.ts @@ -1,4 +1,5 @@ import { Deployment } from "@monitor/types"; +import mongoose from "mongoose"; import { model, Schema } from "mongoose"; export default function deploymentModel() { @@ -7,11 +8,6 @@ export default function deploymentModel() { container: String, }); - const Volume = new Schema({ - variable: String, - value: String, - }); - const EnvironmentVar = new Schema({ variable: String, value: String, @@ -27,7 +23,7 @@ export default function deploymentModel() { image: String, // used if deploying an external image (from docker hub) latest: Boolean, // if custom image, use this to add :latest ports: [Conversion], - volumes: [Volume], + volumes: [Conversion], environment: [EnvironmentVar], network: String, restart: String, @@ -42,3 +38,13 @@ export default function deploymentModel() { return model("Deployment", schema) } + +export async function getCoreDeployment({ name, mongoUrl }: { name: string; mongoUrl: string }) { + await mongoose.connect(mongoUrl); + + const deployments = deploymentModel(); + + return (await deployments.findOne({ name }).lean().exec()) as + | Deployment + | undefined; +} diff --git a/core/src/plugins/ws.ts b/core/src/plugins/ws.ts index 90c8a6da3..28b50f186 100644 --- a/core/src/plugins/ws.ts +++ b/core/src/plugins/ws.ts @@ -27,18 +27,25 @@ const ws = fp((app: FastifyInstance, _: {}, done: () => void) => { } ); - app.get("/ws", { websocket: true }, async (connection) => { + app.get("/ws", { websocket: true }, async (connection) => { connection.socket.on("message", async (msg) => { const jwt = JSON.parse(msg.toString()).token; - if (app.jwt.verify(jwt)) { + if (jwt && app.jwt.verify(jwt)) { const payload = decode(jwt) as { id: string }; const userID = payload.id; const user = await app.users.findById(userID); if (user) { - const unsub = messages.subscribe((msg) => connection.socket.send(msg)); + const unsub = messages.subscribe((msg) => + connection.socket.send(msg) + ); connection.socket.removeAllListeners("message"); connection.socket.on("message", (msg) => - handleMessage(app, connection.socket, JSON.parse(msg.toString()), user) + handleMessage( + app, + connection.socket, + JSON.parse(msg.toString()), + user + ) ); connection.socket.on("close", unsub); connection.socket.send( diff --git a/frontend/src/components/UserInfo.tsx b/frontend/src/components/UserInfo.tsx index f9342dbbc..3f8eb6dfa 100644 --- a/frontend/src/components/UserInfo.tsx +++ b/frontend/src/components/UserInfo.tsx @@ -4,19 +4,24 @@ import Flex from "./util/layout/Flex"; import Grid from "./util/layout/Grid"; import { User } from "@monitor/types"; import { pushNotification } from ".."; +import { useUser } from "../state/UserProvider"; +import { useAppState } from "../state/StateProvider"; -const UserInfo: Component<{ user: User; logout: () => void }> = (p) => { +const UserInfo: Component<{}> = (p) => { + const { user, logout } = useUser(); + const { ws } = useAppState(); return ( -
provider: {getAuthProvider(p.user)}
+
provider: {getAuthProvider(user() as User)}
-
username: {p.user.username}
- - +
username: {user().username}
+ +