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:
commit
d8b2a3bb46
6 changed files with 242 additions and 117 deletions
|
@ -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"
|
||||||
|
}
|
|
@ -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
18
pages/api/containers.tsx
Normal 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
21
pages/api/services.tsx
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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
|
Loading…
Reference in a new issue