diff --git a/core/src/config.ts b/core/src/config.ts index faa655695..ef0e02b53 100644 --- a/core/src/config.ts +++ b/core/src/config.ts @@ -5,6 +5,7 @@ import { readJSONFile, } from "@monitor/util"; +export const CORE_SERVER_NAME = getStringFromEnv("CORE_SERVER_NAME", "Monitor Core"); export const SECRETS = readJSONFile("secrets.json"); export const LOG = getBooleanFromEnv("LOG", false); export const PORT = getNumberFromEnv("PORT", 8000); @@ -15,3 +16,8 @@ export const MONGO_URL = getStringFromEnv( ); export const TOKEN_EXPIRES_IN = getNumberFromEnv("TOKEN_EXPIRES_IN", 3000); export const PASSWORD_SALT_ROUNDS = getNumberFromEnv("PASSWORD_SALT_ROUNDS", 8); +export const SYSROOT = getStringFromEnv("SYSROOT", "/home/ubuntu/"); // the root folder monitor has access to, prepends volumes mounted using useSysroot +export const ROOT = getStringFromEnv("ROOT", "/rootDir/"); // the root folder in the container that SYSROOT is mounted on +export const DEPLOYDATA_ROOT = "deployments/"; +export const REPO_PATH = ROOT + "builds/"; +export const REGISTRY_URL = getStringFromEnv("REGISTRY_URL", "localhost:5000/"); \ No newline at end of file diff --git a/core/src/main.ts b/core/src/main.ts index eae1c205d..d15edbfb3 100644 --- a/core/src/main.ts +++ b/core/src/main.ts @@ -4,10 +4,12 @@ import auth from "./util/auth"; import { LOG, PORT } from "./config"; import db from "./util/db"; import ws from "./util/ws"; +import docker from "./util/docker"; const app = fastify({ logger: LOG }) - .register(fastifyCors) + .register(fastifyCors) .register(db) + .register(docker) .register(auth) .register(ws); diff --git a/core/src/util/db/builds.ts b/core/src/util/db/builds.ts index 2eebe570b..c4342adbe 100644 --- a/core/src/util/db/builds.ts +++ b/core/src/util/db/builds.ts @@ -1,9 +1,10 @@ +import { Build } from "@monitor/types"; import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { Schema } from "mongoose"; const builds = fp((app: FastifyInstance, _: {}, done: () => void) => { - const schema = new Schema({ + const schema = new Schema({ name: { type: String, unique: true, index: true }, /* repo related */ repo: String, diff --git a/core/src/util/db/deployments.ts b/core/src/util/db/deployments.ts index 73694ef49..955c3d9d6 100644 --- a/core/src/util/db/deployments.ts +++ b/core/src/util/db/deployments.ts @@ -1,11 +1,13 @@ +import { Deployment } from "@monitor/types"; import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { Schema } from "mongoose"; import { Conversion, EnvironmentVar, Volume } from "./misc"; const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => { - const schema = new Schema({ + const schema = new Schema({ name: { type: String, unique: true, index: true }, + containerName: { type: String, unique: true, index: true }, // for auto pull of frontend repo as well owner: { type: String, index: true }, serverID: { type: String, index: true }, buildID: { type: String, index: true }, // if deploying a monitor build @@ -22,7 +24,6 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => { /* to manage repo for static frontend, mounted as a volume */ repo: String, accessToken: String, - pullName: { type: String, index: true }, // for auto pull of repo containerMount: String, // the file path to mount repo on inside the container }); diff --git a/core/src/util/db/index.ts b/core/src/util/db/index.ts index 594bbdcb5..083993c8d 100644 --- a/core/src/util/db/index.ts +++ b/core/src/util/db/index.ts @@ -2,11 +2,12 @@ import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import mongoose, { Model } from "mongoose"; import { MONGO_URL } from "../../config"; -import { Build, Deployment, Update, User } from "@monitor/types"; +import { Build, Deployment, Server, Update, User } from "@monitor/types"; import users from "./users"; import updates from "./updates"; import deployments from "./deployments"; import builds from "./builds"; +import servers from "./servers"; declare module "fastify" { interface FastifyInstance { @@ -15,6 +16,7 @@ declare module "fastify" { deployments: Model; builds: Model; updates: Model; + servers: Model; } } @@ -25,6 +27,7 @@ const db = fp((app: FastifyInstance, _: {}, done: () => void) => { app .register(users) + .register(servers) .register(deployments) .register(builds) .register(updates); diff --git a/core/src/util/db/servers.ts b/core/src/util/db/servers.ts new file mode 100644 index 000000000..7aab93561 --- /dev/null +++ b/core/src/util/db/servers.ts @@ -0,0 +1,22 @@ +import { Server } from "@monitor/types"; +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import { Schema } from "mongoose"; + +const servers = fp((app: FastifyInstance, _: {}, done: () => void) => { + const schema = new Schema({ + name: { type: String, unique: true }, + address: String, + password: String, + port: String, + rootDirectory: String, + enabled: Boolean, + useHTTP: Boolean, + }); + + app.decorate("servers", app.mongoose.model("Server", schema)); + + done(); +}); + +export default servers; diff --git a/core/src/util/db/updates.ts b/core/src/util/db/updates.ts index f7c41a807..585a8e19b 100644 --- a/core/src/util/db/updates.ts +++ b/core/src/util/db/updates.ts @@ -1,10 +1,11 @@ +import { Update } from "@monitor/types"; import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { Schema } from "mongoose"; import { Log } from "./misc"; const updates = fp((app: FastifyInstance, _: {}, done: () => void) => { - const schema = new Schema({ + const schema = new Schema({ buildID: { type: String, index: true }, deploymentID: { type: String, index: true }, serverID: { type: String, index: true }, diff --git a/core/src/util/db/users.ts b/core/src/util/db/users.ts index d3b7a19ec..1fe43ca9a 100644 --- a/core/src/util/db/users.ts +++ b/core/src/util/db/users.ts @@ -1,9 +1,10 @@ +import { User } from "@monitor/types"; import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { Schema } from "mongoose"; const users = fp((app: FastifyInstance, _: {}, done: () => void) => { - const schema = new Schema({ + const schema = new Schema({ username: { type: String, index: true, required: true }, permissions: { type: Number, default: 0 }, password: String, diff --git a/core/src/util/docker/build.ts b/core/src/util/docker/build.ts new file mode 100644 index 000000000..8799e7cf5 --- /dev/null +++ b/core/src/util/docker/build.ts @@ -0,0 +1,18 @@ +import { Build } from "@monitor/types"; +import { REGISTRY_URL, REPO_PATH } from "../../config"; +import { toDashedName } from "../helpers"; + +export function createDockerBuild({ + name, + buildPath, + dockerfilePath, + pullName, +}: Build) { + return `cd ${REPO_PATH}${pullName}${ + buildPath ? (buildPath[0] === "/" ? buildPath : "/" + buildPath) : "" + } && docker build -t ${ + REGISTRY_URL + toDashedName(name) + } -f ${dockerfilePath} . && docker push ${ + REGISTRY_URL + toDashedName(name) + }`; +} \ No newline at end of file diff --git a/core/src/util/docker/container.ts b/core/src/util/docker/container.ts new file mode 100644 index 000000000..1c014eef6 --- /dev/null +++ b/core/src/util/docker/container.ts @@ -0,0 +1,46 @@ +import { ContainerStatus, Deployment } from "@monitor/types"; +import { objFrom2Arrays } from "@monitor/util"; +import { FastifyInstance } from "fastify"; + +export async function allContainerStatusLocal( + app: FastifyInstance, + deploymentsAr: Deployment[] +) { + const statusAr = await app.dockerode.listContainers({ all: true }); + const statusNames = statusAr.map((stat) => + stat.Names[0].slice(1, stat.Names[0].length) + ); // they all start with '/' + const status = objFrom2Arrays( + statusNames, + statusAr.map((stat, i) => ({ + name: statusNames[i], + Status: stat.Status, + State: stat.State as "running" | "exited", + })) + ); + return objFrom2Arrays( + statusNames, + statusNames.map((name) => { + const tryDeployment = getDeployment(status[name], deploymentsAr); + const _id = tryDeployment ? tryDeployment._id : "none"; + return { + ...status[name], + deploymentID: _id, + }; + }) + ); +} + +function getDeployment( + status: ContainerStatus, + deploymentsAr: Deployment[] +) { + for (let i = 0; i < deploymentsAr.length; i++) { + if (deploymentsAr[i].containerName === status.name) { + return deploymentsAr[i]; + } + } + return undefined; +} + + diff --git a/core/src/util/docker/index.ts b/core/src/util/docker/index.ts new file mode 100644 index 000000000..f6509d13d --- /dev/null +++ b/core/src/util/docker/index.ts @@ -0,0 +1,16 @@ +import Dockerode from "dockerode"; +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; + +declare module "fastify" { + interface FastifyInstance { + dockerode: Dockerode; + } +} + +const docker = fp((app: FastifyInstance, _: {}, done: () => void) => { + app.decorate("dockerode", new Dockerode()); + done(); +}); + +export default docker; \ No newline at end of file diff --git a/core/src/util/docker/run.ts b/core/src/util/docker/run.ts new file mode 100644 index 000000000..91cbc1c54 --- /dev/null +++ b/core/src/util/docker/run.ts @@ -0,0 +1,87 @@ +import { Build, Conversion, Deployment, EnvironmentVar, Volume } from "@monitor/types"; +import { FastifyInstance } from "fastify"; +import { DEPLOYDATA_ROOT, REGISTRY_URL, SYSROOT } from "../../config"; + +export async function createDockerRun( + app: FastifyInstance, + { + buildID, + image, + latest, + ports, + environment, + network, + volumes, + restart, + postImage, + containerName, + containerUser, + }: Deployment, +) { + const _image = buildID + ? REGISTRY_URL + + ((await app.builds.findById(buildID)) as any as Build).imageName + : image; + return ( + `docker pull ${_image}${buildID || latest ? ":latest" : ""} && ` + + `docker run -d --name ${containerName}` + + containerUserString(containerUser) + + portsString(ports) + + volsString(containerName!, volumes) + + envString(environment) + + restartString(restart) + + networkString(network) + + ` ${_image}${buildID || latest ? ":latest" : ""}${ + postImage ? " " + postImage : "" + }` + ); +} + +function portsString(ports?: Conversion[]) { + return ports && ports.length > 0 + ? ports + .map(({ local, container }) => ` -p ${local}:${container}`) + .reduce((prev, curr) => prev + curr) + : ""; +} + +function volsString(folderName: string, volumes?: Volume[]) { + return volumes && volumes.length > 0 + ? volumes + .map(({ local, container, useSystemRoot }) => { + const mid = useSystemRoot ? "" : `${DEPLOYDATA_ROOT}${folderName}/`; + const localString = + local.length > 0 + ? local[0] === "/" + ? local.slice(1, local.length) + : local + : ""; + return ` -v ${SYSROOT + mid + localString}:${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}` + : ""; +} diff --git a/core/src/util/helpers.ts b/core/src/util/helpers.ts new file mode 100644 index 000000000..f2ecc14b6 --- /dev/null +++ b/core/src/util/helpers.ts @@ -0,0 +1,19 @@ +import { Deployment, Server } from "@monitor/types"; +import { FastifyInstance } from "fastify"; + +export function toDashedName(name: string) { + return name.toLowerCase().replaceAll(" ", "-"); +} + +export async function getDeploymentAndServer( + app: FastifyInstance, + deploymentID: string +) { + const deployment = (await app.deployments.findById( + deploymentID + )) as Deployment; + return { + deployment, + server: (await app.servers.findById(deployment.serverID)) as Server, + }; +} diff --git a/core/tsconfig.json b/core/tsconfig.json index 63c3dc0ec..6f7cc354a 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -6,7 +6,8 @@ "es2019", "es2020.promise", "es2020.bigint", - "es2020.string" + "es2020.string", + "ESNext" ], "module": "commonjs", "target": "es2019", diff --git a/types/types.d.ts b/types/types.d.ts index b349bdb3a..7cb88a87d 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -86,7 +86,7 @@ export type Deployment = { /* to manage repo for static frontend, mounted as a volume */ repo?: string; accessToken?: string; - pullName?: string; // for auto pull of repo + containerName?: string; // also for auto pull of frontend repo containerMount?: string; // the file path to mount repo on inside the container }; diff --git a/util/helpers.ts b/util/helpers.ts index 08d3a1a14..2070d626a 100644 --- a/util/helpers.ts +++ b/util/helpers.ts @@ -1,3 +1,4 @@ +import { Collection } from "@monitor/types"; import { readFileSync } from "fs"; export function readJSONFile(path: string): T { @@ -19,3 +20,11 @@ export function getBooleanFromEnv(varName: string, defaultValue: boolean) { else if (variable === "false") return false; else return defaultValue; } + +export function objFrom2Arrays(keys: string[], entries: T[]): Collection { + return Object.fromEntries( + keys.map((id, index) => { + return [id, entries[index]]; + }) + ); +}