Merge branch 'rewrite/services' into 'main'

Closes #10, #2

Closes #1, #2, and #10

See merge request neshura-websites/www!5
This commit is contained in:
Neshura 2022-12-11 16:15:41 +00:00
commit d8b2a3bb46
6 changed files with 242 additions and 117 deletions

View file

@ -1,15 +1,42 @@
export interface LinkList { export interface EntryList {
services: CustomLink[], services: Service[],
games: CustomLink[] games: CustomLink[]
} }
export interface CustomLink { export interface CustomLink {
type: string,
name: string, name: string,
href: string, href: string,
desc: string, desc: string,
warn: string,
ip: string, ip: string,
type: string,
location: string, location: string,
status: string, status: string,
docker_container_name: string docker_container_name: string
} }
export interface Service {
name: string,
href: string,
desc: string,
warn: string,
type: ServiceType,
docker_container_name: string,
location: ServiceLocation,
status: ServiceStatus
}
export enum ServiceStatus {
online = "Online",
offline = "Offline",
loading = "Loading",
error = "ERROR"
}
export enum ServiceLocation {
brr7_4800u = "brr7-4800u",
other = ""
}
export enum ServiceType {
docker = "docker",
app = "app"
}

View file

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev:debug": "NODE_OPTIONS='--inspect' next dev -p 4040", "dev:debug": "NODE_OPTIONS='--inspect' next dev -p 4040",
"dev": "next dev", "dev": "next dev -p 4040",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
@ -17,10 +17,10 @@
"swr": "^1.3.0" "swr": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.23.1",
"eslint-config-next": "12.2.0",
"@types/dockerode": "^3.3.14", "@types/dockerode": "^3.3.14",
"@types/react": "^18.0.14", "@types/react": "^18.0.14",
"eslint": "^8.23.1",
"eslint-config-next": "12.2.0",
"typescript": "^4.7.4" "typescript": "^4.7.4"
} }
} }

18
pages/api/containers.tsx Normal file
View file

@ -0,0 +1,18 @@
import Docker from 'dockerode'
export default async function ContainersAPI(req: any, res: any) {
try {
const options = {
socketPath: '/var/run/docker.sock',
path: '/v1.41/containers/json'
};
var docker = new Docker({ socketPath: options.socketPath });
const list = await docker.listContainers({ all: true })
res.status(200).json(list);
}
catch (error) {
console.log(error);
res.status(500).json({ error: 'Error reading data' });
}
}

21
pages/api/services.tsx Normal file
View file

@ -0,0 +1,21 @@
import fsPromises from 'fs/promises'
import path from 'path'
import { Service, ServiceStatus } from '../../interfaces/LinkTypes';
export default async function ServicesAPI(req: any, res: any) {
try {
const filePath = path.join(process.cwd(), '/public/pages.json')
const data = await fsPromises.readFile(filePath)
.then((file) => JSON.parse(file.toString()));
data.services.forEach((service: Service) => {
service.status = ServiceStatus.loading;
});
res.status(200).json(data.services);
}
catch (error) {
console.log(error);
res.status(500).json({ error: 'Error reading data' });
}
}

View file

@ -3,9 +3,9 @@ import Link from 'next/link'
import styles from '/styles/Home.module.css' import styles from '/styles/Home.module.css'
import fsPromises from 'fs/promises' import fsPromises from 'fs/promises'
import path from 'path' import path from 'path'
import type { CustomLink, LinkList } from '../interfaces/LinkTypes' import type { CustomLink, EntryList } from '../interfaces/LinkTypes'
function Servers(props: LinkList) { function Servers(props: EntryList) {
const serverList = props.games const serverList = props.games
return ( return (
<> <>

View file

@ -1,20 +1,65 @@
import Head from 'next/head' import Head from 'next/head'
import Link from 'next/link' import Link from 'next/link'
import styles from '/styles/Home.module.css' import styles from '/styles/Home.module.css'
import fsPromises from 'fs/promises' import { Service, ServiceStatus, ServiceType, ServiceLocation } from '../interfaces/LinkTypes';
import path, { resolve } from 'path'
import type { CustomLink, LinkList } from '../interfaces/LinkTypes'
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { ReactElement } from 'react'
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((res) => res.json())
//function Services(props: EntryList) {
function Services() {
const { initialData, fullData, loadingInitial, loadingFull, error } = useServices();
function Services(props: LinkList) { let content: ReactElement = <></>;
const serviceList = props.services
if (error) { content = <div>Error loading data</div> }
else if (loadingInitial) {
content = <div>Loading</div>
}
else if (loadingFull) {
content =
<div className={styles.grid}>
{initialData?.map((item: Service) => (
<Link key={item.name} href={item.href}>
<a className={styles.contentcard}>
<div className={styles.contenttitle}><h2>{item.name}</h2></div>
<div className={item.status === ServiceStatus.online ? styles.contentonline : styles.contentoffline}>{item.status}</div>
<div><p>{item.desc}</p></div>
<div className={styles.cardwarn}><p>{item.warn}</p></div>
</a>
</Link>
))}
</div>
}
else if (fullData) {
content =
<div className={styles.grid}>
{fullData.map((item: Service) => (
<Link key={item.name} href={item.href}>
<a className={styles.contentcard}>
<div className={styles.contenttitle}><h2>{item.name}</h2></div>
<div className={item.status === ServiceStatus.online ? styles.contentonline : styles.contentoffline}>{item.status}</div>
<div><p>{item.desc}</p></div>
<div className={styles.cardwarn}><p>{item.warn}</p></div>
</a>
</Link>
))}
</div>
}
else {
content = <div>Error loading data</div>
}
return ( return (
<> <>
<Head> <Head>
<title>Neshura Servers</title> <title>Neshura Servers</title>
<meta charSet='utf-8' /> <meta charSet='utf-8' />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="description" content="Lists all available Services, most likely up-to-date" />
</Head> </Head>
<h1 className={styles.title}> <h1 className={styles.title}>
@ -24,116 +69,130 @@ function Services(props: LinkList) {
<p className={styles.description}> <p className={styles.description}>
Lists all available Services, most likely up-to-date Lists all available Services, most likely up-to-date
</p> </p>
<div className={styles.grid}>
{serviceList.map((item: CustomLink) => ( {content}
<Link key={item.name} href={item.href}>
<a className={styles.contentcard}>
<div className={styles.contenttitle}><h2>{item.name}</h2></div>
<div className={item.status == "Online" ? styles.contentonline : styles.contentoffline}>{item.status}</div>
<div><p>{item.desc}</p></div>
<div className={styles.cardwarn}><p>{item.warn}</p></div>
</a>
</Link>
))}
</div>
</> </>
) )
} }
// Gets a List of all services specified in /public/pages.json async function getStatus(entry: Service, containers: Dockerode.ContainerInfo[]) {
export async function getServerSideProps() { // Currently the only location supporting different fetching depending on type is brr7-4800u
const filePath = path.join(process.cwd(), '/public/pages.json') // Others to follow but low prio as this is currently the only location used
// TODO: look into asyncing this API call
const jsonData = await fsPromises.readFile(filePath) // Location BRR7-4800U
const list = JSON.parse(jsonData.toString()) if (entry.location === ServiceLocation.brr7_4800u) {
for (let index = 0; index < list.services.length; index++) { // Type APP
// TODO: look into asyncing this as well if (entry.type === ServiceType.app) {
await status(list.services[index]); await fetch(entry.href)
.then((response) => {
if (response.ok) {
switch (response.status) {
case 200:
case 301:
case 302:
entry.status = ServiceStatus.online;
break;
default:
entry.status = ServiceStatus.offline;
}
}
else {
entry.status = ServiceStatus.offline;
}
})
.catch((error) => {
console.error("Error pinging Website: ", error);
entry.status = ServiceStatus.error;
})
}
// Type Docker
else if (entry.type === ServiceType.docker) {
for (let i = 0; i < containers.length; i++) {
const container = containers[i];
// Docker API returns container names with / prepended
if (containers[i].Names.includes("/" + entry.docker_container_name)) {
// so far only "running" is properly implemented, mroe cases to follow as needed
switch (container.State) {
case "running":
entry.status = ServiceStatus.online;
break;
default:
console.log("Container Status " + container.State + " has no case implemented");
entry.status = ServiceStatus.offline;
}
// cancel the for
break;
}
// If container name is not missing the container is set to offline
else if (entry.docker_container_name !== null) {
console.warn("Container for " + entry.name + " could not be found");
entry.status = ServiceStatus.offline;
}
else {
console.error("Container Name not specified");
entry.status = ServiceStatus.error;
}
}
}
// If no Type matches
else {
console.warn("Service Type for Service " + entry.name + " not specified or invalid");
entry.status = ServiceStatus.error;
}
} }
return { props: list } // Location Other
// TODO: implement docker type for other locations
else if (entry.location === ServiceLocation.other) {
// Currently uses the same handling as app type for the other location
await fetch(entry.href)
.then((response) => {
if (response.ok) {
switch (response.status) {
case 200:
case 301:
case 302:
entry.status = ServiceStatus.online;
break;
default:
entry.status = ServiceStatus.offline;
}
}
else {
entry.status = ServiceStatus.offline;
}
})
.catch((error) => {
console.error("Error pinging Website: ", error);
entry.status = ServiceStatus.error;
})
}
// If no Location matches
else {
console.warn("Service Location for Service " + entry.name + " not specified");
entry.status = ServiceStatus.error;
}
return entry;
} }
// reversing this to loop over given entries for every found container would probably improve latency const fetchFullDataArray = (containerData: Dockerode.ContainerInfo[], dataSet: Service[]) => {
async function status(entry: CustomLink) { const fetchStatus = (entry: Service) => getStatus(entry, containerData);
// for now only the BRR7-4800U can be used with Docker, needs changing once more Servers are used return Promise.all(dataSet.map(fetchStatus));
// TODO: support multiple locations for docker }
if (entry.location === "brr7-4800u") {
// app in this context means any non-docker page
if (entry.type === "app") {
let data = new Response(); function useServices() {
try { const { data: containerData, error: containerError } = useSWR('/api/containers', fetcher);
await fetch(entry.href).then((response: Response) => data = response); const { data: initialData, error: initialError } = useSWR('/api/services', fetcher);
} const loadingInitial = !initialData && !initialError
catch (e) { const { data: fullData, error: fullError } = useSWR((initialData && containerData) ? [containerData, initialData] : null, fetchFullDataArray)
console.log(e) const loadingFull = !fullData && !fullError
return (entry.status = "Offline");
}
if (data.ok) { return {
if (data.status == 200 || data.status == 301 || data.status == 302) { initialData,
return (entry.status = "Online"); fullData,
} loadingInitial,
else return (entry.status = "Offline"); loadingFull,
} error: initialError || fullError || containerError,
else return (entry.status = "Offline"); };
}
else if (entry.type === "docker") {
var Docker = require('dockerode');
// TODO: read these paths from some config instead of hardcoding them
const options = {
socketPath: '/var/run/docker.sock',
path: '/v1.41/containers/json'
};
var docker = new Docker({ socketPath: options.socketPath });
// default is set as Offline, prevents uncaught cases without set status
entry.status = "Offline";
// TODO: async possible?
await docker.listContainers({all: true}).then(
((containers: Dockerode.ContainerInfo[]) => {
// Loop over every found container and compare to the entry provided
containers.forEach((element: Dockerode.ContainerInfo) => {
element.Names.forEach((containerName: string) => {
if (containerName.startsWith("/")) {
containerName = containerName.substring(1);
}
if (containerName === entry.docker_container_name) {
entry.status = "Online";
}
});
if (entry.docker_container_name == null) {
console.log("MISSING DOCKER CONTAINER NAME FOR " + entry.name);
entry.status = "ERROR";
}
});
})
);
}
else { entry.status = "ERROR" }
return;
}
// for non-local locations pinging can be used to see if they're up
else if (entry.location != "") {
let data: Response;
try {
data = await fetch(entry.href);
}
catch (e) {
console.log(e)
return (entry.status = "Offline");
}
if (data.ok) {
if (data.status == 200 || data.status == 301 || data.status == 302) {
return (entry.status = "Online");
}
else return (entry.status = "Offline");
}
else return (entry.status = "Offline");
}
else { return (entry.status = "ERROR") }
} }
export default Services export default Services