diff --git a/app/checkRunner.js b/app/checkRunner.js new file mode 100644 index 0000000..4ee95a1 --- /dev/null +++ b/app/checkRunner.js @@ -0,0 +1,47 @@ +const { spawn } = require("child_process"); +const path = require("path"); +const dockerApi = require("./dockerApi"); + +const CHECKS_DIR = path.join(__dirname, "checks"); + +async function runCheck(check) { + const start = Date.now(); + + if (check.type === "docker_container") { + const result = await dockerApi.isContainerRunning(check.target); + return { + status: result ? "OK" : "FAIL", + message: `Container ${check.target} running: ${result}`, + duration: Date.now() - start + }; + } + + if (check.type === "docker_network") { + const [network, container] = check.target.split(","); + const result = await dockerApi.networkContainsContainer(network, container); + return { + status: result ? "OK" : "FAIL", + message: `Container ${container} in network ${network}: ${result}`, + duration: Date.now() - start + }; + } + + if (check.type === "script") { + return new Promise((resolve) => { + const scriptPath = path.join(CHECKS_DIR, check.script_name); + const child = spawn("sh", [scriptPath], { + timeout: check.timeout_seconds * 1000 + }); + + child.on("close", (code) => { + resolve({ + status: code === 0 ? "OK" : "FAIL", + message: `Exit code: ${code}`, + duration: Date.now() - start + }); + }); + }); + } +} + +module.exports = runCheck; diff --git a/app/checks/check_http_google.sh b/app/checks/check_http_google.sh new file mode 100644 index 0000000..2f38fd2 --- /dev/null +++ b/app/checks/check_http_google.sh @@ -0,0 +1,3 @@ +#!/bin/sh +wget -q --spider https://google.com +exit $? diff --git a/app/db.js b/app/db.js new file mode 100644 index 0000000..c24395b --- /dev/null +++ b/app/db.js @@ -0,0 +1,9 @@ +const { Pool } = require("pg"); + +module.exports = new Pool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT +}); diff --git a/app/dockerApi.js b/app/dockerApi.js new file mode 100644 index 0000000..f505d6b --- /dev/null +++ b/app/dockerApi.js @@ -0,0 +1,48 @@ +const http = require("http"); + +function dockerRequest(path) { + return new Promise((resolve, reject) => { + const options = { + socketPath: "/var/run/docker.sock", + path, + method: "GET" + }; + + const req = http.request(options, (res) => { + let data = ""; + res.on("data", chunk => data += chunk); + res.on("end", () => { + try { + resolve(JSON.parse(data)); + } catch { + resolve(data); + } + }); + }); + + req.on("error", reject); + req.end(); + }); +} + +async function isContainerRunning(name) { + try { + const data = await dockerRequest(`/containers/${name}/json`); + return data.State?.Running === true; + } catch { + return false; + } +} + +async function networkContainsContainer(networkName, containerName) { + try { + const data = await dockerRequest(`/networks/${networkName}`); + if (!data.Containers) return false; + return Object.values(data.Containers) + .some(c => c.Name === containerName); + } catch { + return false; + } +} + +module.exports = { isContainerRunning, networkContainsContainer }; diff --git a/app/init.sh b/app/init.sh new file mode 100644 index 0000000..97d5c7e --- /dev/null +++ b/app/init.sh @@ -0,0 +1,2 @@ +chmod +x checks/*.sh +docker compose up --build \ No newline at end of file diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..4e08df5 --- /dev/null +++ b/app/package.json @@ -0,0 +1,11 @@ +{ + "name": "docker-info-dashboard", + "version": "2.0.0", + "main": "server.js", + "dependencies": { + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-cron": "^3.0.3", + "pg": "^8.11.3" + } +} diff --git a/app/public/style.css b/app/public/style.css new file mode 100644 index 0000000..8c0548a --- /dev/null +++ b/app/public/style.css @@ -0,0 +1,5 @@ +body { font-family: Arial; } +table { border-collapse: collapse; width: 80%; } +th, td { border: 1px solid #ccc; padding: 8px; } +.ok { color: green; font-weight: bold; } +.fail { color: red; font-weight: bold; } diff --git a/app/scheduler.js b/app/scheduler.js new file mode 100644 index 0000000..0a99b46 --- /dev/null +++ b/app/scheduler.js @@ -0,0 +1,26 @@ +const cron = require("node-cron"); +const pool = require("./db"); +const runCheck = require("./checkRunner"); + +async function scheduleChecks() { + const { rows } = await pool.query("SELECT * FROM checks WHERE active = true"); + + rows.forEach(check => { + cron.schedule(`*/${check.schedule_seconds} * * * * *`, async () => { + const result = await runCheck(check); + + await pool.query( + `INSERT INTO results (check_id, status, message, duration_ms) + VALUES ($1,$2,$3,$4)`, + [check.id, result.status, result.message, result.duration] + ); + + await pool.query( + `UPDATE checks SET last_run = NOW(), last_status = $1 WHERE id = $2`, + [result.status, check.id] + ); + }); + }); +} + +module.exports = scheduleChecks; diff --git a/app/server.js b/app/server.js new file mode 100644 index 0000000..243573d --- /dev/null +++ b/app/server.js @@ -0,0 +1,17 @@ +const express = require("express"); +const pool = require("./db"); +const scheduleChecks = require("./scheduler"); + +const app = express(); +app.set("view engine", "ejs"); +app.use(express.static("public")); + +app.get("/", async (req, res) => { + const { rows } = await pool.query("SELECT * FROM checks ORDER BY id"); + res.render("index", { checks: rows }); +}); + +app.listen(3000, async () => { + console.log("Server läuft auf Port 3000"); + await scheduleChecks(); +}); diff --git a/app/views/index.ejs b/app/views/index.ejs new file mode 100644 index 0000000..28ad693 --- /dev/null +++ b/app/views/index.ejs @@ -0,0 +1,30 @@ + + + + Docker Info Dashboard + + + +

System Status

+ + + + + + + + +<% checks.forEach(c => { %> + + + + + + +<% }) %> + +
IDNameStatusLast Run
<%= c.id %><%= c.name %> +<%= c.last_status || 'N/A' %> +<%= c.last_run || 'N/A' %>
+ + diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 0000000..75ba892 --- /dev/null +++ b/db/init.sql @@ -0,0 +1,28 @@ +CREATE TABLE checks ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, -- script | docker_container | docker_network + description TEXT, + target TEXT, + script_name TEXT, + schedule_seconds INTEGER DEFAULT 60, + timeout_seconds INTEGER DEFAULT 20, + active BOOLEAN DEFAULT TRUE, + last_run TIMESTAMP, + last_status TEXT +); + +CREATE TABLE results ( + id SERIAL PRIMARY KEY, + check_id INTEGER REFERENCES checks(id) ON DELETE CASCADE, + run_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT, + message TEXT, + duration_ms INTEGER +); + +INSERT INTO checks (name, type, description, target) +VALUES +('Google HTTP', 'script', 'Prüft google.com', NULL), +('Postgres Container Running', 'docker_container', 'Ist Postgres Container aktiv?', 'info-postgres'), +('Default Network contains Postgres', 'docker_network', 'Ist Postgres im default Netzwerk?', 'bridge'); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..64b2acb --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,37 @@ +version: "3.9" + +services: + app: + image: node:20-alpine + container_name: info-node-app + working_dir: /app + command: sh -c "npm install && node server.js" + volumes: + - ./app:/app + - ./checks:/app/checks + - /var/run/docker.sock:/var/run/docker.sock + ports: + - "3000:3000" + depends_on: + - db + environment: + DB_HOST: db + DB_USER: postgres + DB_PASSWORD: postgres + DB_NAME: infodb + DB_PORT: 5432 + restart: unless-stopped + + db: + image: postgres:16-alpine + container_name: info-postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: infodb + volumes: + - ./pgdata:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432"