diff --git a/Server/db/models/Monitor.js b/Server/db/models/Monitor.js index 111c48d03..d44566a97 100644 --- a/Server/db/models/Monitor.js +++ b/Server/db/models/Monitor.js @@ -28,7 +28,7 @@ const MonitorSchema = mongoose.Schema( type: { type: String, required: true, - enum: ["http", "ping", "pagespeed", "hardware"], + enum: ["http", "ping", "pagespeed", "hardware", "docker"], }, url: { type: String, diff --git a/Server/index.js b/Server/index.js index 11b5b2151..0e9811a43 100644 --- a/Server/index.js +++ b/Server/index.js @@ -28,6 +28,7 @@ import NetworkService from "./service/networkService.js"; import axios from "axios"; import ping from "ping"; import http from "http"; +import Docker from "dockerode"; // Email service and dependencies import EmailService from "./service/emailService.js"; @@ -45,7 +46,7 @@ import NotificationService from "./service/notificationService.js"; import db from "./db/mongo/MongoDB.js"; const SERVICE_NAME = "Server"; -const SHUTDOWN_TIMEOUT = 0; +const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; const __filename = fileURLToPath(import.meta.url); @@ -130,7 +131,7 @@ const startApp = async () => { nodemailer, logger ); - const networkService = new NetworkService(axios, ping, logger, http); + const networkService = new NetworkService(axios, ping, logger, http, Docker); const statusService = new StatusService(db, logger); const notificationService = new NotificationService(emailService, db, logger); const jobQueue = await JobQueue.createJobQueue( diff --git a/Server/package-lock.json b/Server/package-lock.json index d03515453..22ffaa863 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -14,6 +14,7 @@ "bcrypt": "^5.1.1", "bullmq": "5.25.6", "cors": "^2.8.5", + "dockerode": "4.0.2", "dotenv": "^16.4.5", "express": "^4.19.2", "handlebars": "^4.7.8", @@ -89,6 +90,12 @@ "node": ">=6.9.0" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -996,6 +1003,15 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1032,6 +1048,26 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -1045,6 +1081,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1056,6 +1101,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1152,6 +1208,30 @@ "node": ">=16.20.1" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1163,6 +1243,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bullmq": { "version": "5.25.6", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.6.tgz", @@ -1708,6 +1797,20 @@ } } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -2014,6 +2117,58 @@ "node": ">=0.3.1" } }, + "node_modules/docker-modem": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", + "integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/docker-modem/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/dockerode": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz", + "integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "docker-modem": "^5.0.3", + "tar-fs": "~2.0.1" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2145,6 +2300,15 @@ "node": ">=0.10.0" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2439,6 +2603,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2822,6 +2992,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -4129,6 +4319,12 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mocha": { "version": "10.8.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", @@ -4503,6 +4699,13 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -5553,6 +5756,16 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6046,6 +6259,29 @@ "memory-pager": "^1.0.2" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.20.0" + } + }, "node_modules/ssl-checker": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/ssl-checker/-/ssl-checker-2.0.10.tgz", @@ -6248,6 +6484,40 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -6311,6 +6581,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", diff --git a/Server/package.json b/Server/package.json index 16e0e9d1c..ce34ee0f2 100644 --- a/Server/package.json +++ b/Server/package.json @@ -17,6 +17,7 @@ "bcrypt": "^5.1.1", "bullmq": "5.25.6", "cors": "^2.8.5", + "dockerode": "4.0.2", "dotenv": "^16.4.5", "express": "^4.19.2", "handlebars": "^4.7.8", diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index aaab725e5..eedeb1a5c 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -180,8 +180,8 @@ class JobQueue { } catch (error) { this.logger.error({ message: error.message, - service: SERVICE_NAME, - method: "createWorker", + service: error.service ?? SERVICE_NAME, + method: error.method ?? "createJobHandler", details: `Error processing job ${job.id}: ${error.message}`, stack: error.stack, }); diff --git a/Server/service/networkService.js b/Server/service/networkService.js index fa665e1ea..23b3a62b1 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -8,11 +8,12 @@ import { errorMessages, successMessages } from "../utils/messages.js"; * @param {Object} http - The HTTP utility for network operations. */ class NetworkService { - constructor(axios, ping, logger, http) { + constructor(axios, ping, logger, http, Docker) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; this.TYPE_HARDWARE = "hardware"; + this.TYPE_DOCKER = "docker"; this.SERVICE_NAME = "NetworkService"; this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; @@ -20,6 +21,7 @@ class NetworkService { this.ping = ping; this.logger = logger; this.http = http; + this.Docker = Docker; } /** @@ -62,28 +64,34 @@ class NetworkService { * @property {string} message - The message indicating the result of the ping request. */ async requestPing(job) { - const url = job.data.url; - const { response, responseTime, error } = await this.timeRequest(() => - this.ping.promise.probe(url) - ); + try { + const url = job.data.url; + const { response, responseTime, error } = await this.timeRequest(() => + this.ping.promise.probe(url) + ); - const pingResponse = { - monitorId: job.data._id, - type: "ping", - responseTime, - payload: response, - }; - if (error) { - pingResponse.status = false; - pingResponse.code = this.PING_ERROR; - pingResponse.message = errorMessages.PING_CANNOT_RESOLVE; + const pingResponse = { + monitorId: job.data._id, + type: "ping", + responseTime, + payload: response, + }; + if (error) { + pingResponse.status = false; + pingResponse.code = this.PING_ERROR; + pingResponse.message = errorMessages.PING_CANNOT_RESOLVE; + return pingResponse; + } + + pingResponse.code = 200; + pingResponse.status = response.alive; + pingResponse.message = successMessages.PING_SUCCESS; return pingResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestPing"; + throw error; } - - pingResponse.code = 200; - pingResponse.status = response.alive; - pingResponse.message = successMessages.PING_SUCCESS; - return pingResponse; } /** @@ -104,34 +112,40 @@ class NetworkService { * @property {string} message - The message indicating the result of the HTTP request. */ async requestHttp(job) { - const url = job.data.url; - const config = {}; + try { + const url = job.data.url; + const config = {}; - job.data.secret !== undefined && - (config.headers = { Authorization: `Bearer ${job.data.secret}` }); + job.data.secret !== undefined && + (config.headers = { Authorization: `Bearer ${job.data.secret}` }); - const { response, responseTime, error } = await this.timeRequest(() => - this.axios.get(url, config) - ); + const { response, responseTime, error } = await this.timeRequest(() => + this.axios.get(url, config) + ); - const httpResponse = { - monitorId: job.data._id, - type: job.data.type, - responseTime, - payload: response?.data, - }; + const httpResponse = { + monitorId: job.data._id, + type: job.data.type, + responseTime, + payload: response?.data, + }; - if (error) { - const code = error.response?.status || this.NETWORK_ERROR; - httpResponse.code = code; - httpResponse.status = false; - httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + if (error) { + const code = error.response?.status || this.NETWORK_ERROR; + httpResponse.code = code; + httpResponse.status = false; + httpResponse.message = this.http.STATUS_CODES[code] || "Network Error"; + return httpResponse; + } + httpResponse.status = true; + httpResponse.code = response.status; + httpResponse.message = this.http.STATUS_CODES[response.status]; return httpResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestHttp"; + throw error; } - httpResponse.status = true; - httpResponse.code = response.status; - httpResponse.message = this.http.STATUS_CODES[response.status]; - return httpResponse; } /** @@ -151,15 +165,114 @@ class NetworkService { * @property {string} message - The message indicating the result of the PageSpeed request. */ async requestPagespeed(job) { - const url = job.data.url; - const updatedJob = { ...job }; - const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; - updatedJob.data.url = pagespeedUrl; - return this.requestHttp(updatedJob); + try { + const url = job.data.url; + const updatedJob = { ...job }; + const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; + updatedJob.data.url = pagespeedUrl; + return await this.requestHttp(updatedJob); + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestPagespeed"; + throw error; + } } + /** + * Sends an HTTP request to check hardware status and returns the response. + * + * @param {Object} job - The job object containing the data for the hardware request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.url - The URL to send the hardware status request to. + * @param {string} job.data._id - The monitor ID for the hardware request. + * @param {string} job.data.type - The type of request, which is "hardware". + * @returns {Promise} An object containing the hardware status response details. + * @property {string} monitorId - The monitor ID for the hardware request. + * @property {string} type - The type of request ("hardware"). + * @property {number} responseTime - The time taken for the request to complete, in milliseconds. + * @property {Object} payload - The response payload from the hardware status request. + * @property {boolean} status - The status of the request (true if successful, false otherwise). + * @property {number} code - The response code (200 if successful, error code otherwise). + * @property {string} message - The message indicating the result of the hardware status request. + */ async requestHardware(job) { - return this.requestHttp(job); + try { + return await this.requestHttp(job); + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestHardware"; + throw error; + } + } + + /** + * Sends a request to inspect a Docker container and returns its status. + * + * @param {Object} job - The job object containing the data for the Docker request. + * @param {Object} job.data - The data object within the job. + * @param {string} job.data.url - The container ID or name to inspect. + * @param {string} job.data._id - The monitor ID for the Docker request. + * @param {string} job.data.type - The type of request, which is "docker". + * @returns {Promise} An object containing the Docker container status details. + * @property {string} monitorId - The monitor ID for the Docker request. + * @property {string} type - The type of request ("docker"). + * @property {number} responseTime - The time taken for the Docker inspection to complete, in milliseconds. + * @property {boolean} status - The status of the container (true if running, false otherwise). + * @property {number} code - The response code (200 if successful, error code otherwise). + * @property {string} message - The message indicating the result of the Docker inspection. + */ + async requestDocker(job) { + try { + const docker = new this.Docker({ + socketPath: "/var/run/docker.sock", + handleError: true, // Enable error handling + }); + + const containers = await docker.listContainers({ all: true }); + const containerExists = containers.some((c) => c.Id === job.data.url); + if (!containerExists) { + throw new Error(errorMessages.DOCKER_NOT_FOUND); + } + const container = docker.getContainer(job.data.url); + + const { response, responseTime, error } = await this.timeRequest(() => + container.inspect() + ); + + const dockerResponse = { + monitorId: job.data._id, + type: job.data.type, + responseTime, + }; + + if (error) { + dockerResponse.status = false; + dockerResponse.code = error.statusCode || this.NETWORK_ERROR; + dockerResponse.message = error.reason || errorMessages.DOCKER_FAIL; + return dockerResponse; + } + dockerResponse.status = response?.State?.Status === "running" ? true : false; + dockerResponse.code = 200; + dockerResponse.message = successMessages.DOCKER_SUCCESS; + return dockerResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestDocker"; + throw error; + } + } + + /** + * Handles unsupported job types by throwing an error with details. + * + * @param {string} type - The unsupported job type that was provided + * @throws {Error} An error with service name, method name and unsupported type message + */ + handleUnsupportedType(type) { + const err = new Error(`Unsupported type: ${type}`); + err.service = this.SERVICE_NAME; + err.method = "getStatus"; + throw err; } /** @@ -172,7 +285,8 @@ class NetworkService { * @throws {Error} Throws an error if the job type is unsupported. */ async getStatus(job) { - switch (job.data.type) { + const type = job?.data?.type ?? "unknown"; + switch (type) { case this.TYPE_PING: return await this.requestPing(job); case this.TYPE_HTTP: @@ -181,13 +295,10 @@ class NetworkService { return await this.requestPagespeed(job); case this.TYPE_HARDWARE: return await this.requestHardware(job); + case this.TYPE_DOCKER: + return await this.requestDocker(job); default: - this.logger.error({ - message: `Unsupported type: ${job.data.type}`, - service: this.SERVICE_NAME, - method: "getStatus", - }); - return {}; + return this.handleUnsupportedType(type); } } } diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 22254d050..4f7c1a4ce 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -133,6 +133,7 @@ class StatusService { ping: this.db.createCheck, pagespeed: this.db.createPageSpeedCheck, hardware: this.db.createHardwareCheck, + docker: this.db.createCheck, }; const operation = operationMap[networkResponse.type]; diff --git a/Server/tests/services/networkService.test.js b/Server/tests/services/networkService.test.js index 4e48524e2..d5c177d6f 100644 --- a/Server/tests/services/networkService.test.js +++ b/Server/tests/services/networkService.test.js @@ -2,8 +2,9 @@ import sinon from "sinon"; import NetworkService from "../../service/networkService.js"; import { expect } from "chai"; import http from "http"; +import { errorMessages } from "../../utils/messages.js"; describe("Network Service", () => { - let axios, ping, logger, networkService; + let axios, ping, Docker, logger, networkService; beforeEach(() => { axios = { @@ -12,6 +13,17 @@ describe("Network Service", () => { status: 200, }), }; + Docker = class { + listContainers = sinon.stub().resolves([ + { + Names: ["http://test.com"], + Id: "http://test.com", + }, + ]); + getContainer = sinon.stub().returns({ + inspect: sinon.stub().resolves({ State: { Status: "running" } }), + }); + }; ping = { promise: { probe: sinon @@ -20,7 +32,7 @@ describe("Network Service", () => { }, }; logger = { error: sinon.stub() }; - networkService = new NetworkService(axios, ping, logger, http); + networkService = new NetworkService(axios, ping, logger, http, Docker); }); describe("constructor", () => { it("should create a new NetworkService instance", () => { @@ -70,6 +82,18 @@ describe("Network Service", () => { expect(pingResult.status).to.be.false; expect(pingResult.code).to.equal(networkService.PING_ERROR); }); + it("should throw an error if ping cannot resolve", async () => { + const error = new Error("test error"); + networkService.timeRequest = sinon.stub().throws(error); + try { + await networkService.requestPing({ + data: { url: "http://test.com", _id: "123" }, + }); + } catch (error) { + expect(error).to.exist; + expect(error.method).to.equal("requestPing"); + } + }); }); describe("requestHttp", () => { it("should return a response object if http successful", async () => { @@ -108,6 +132,18 @@ describe("Network Service", () => { expect(httpResult.status).to.be.false; expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); }); + it("should throw an error if an error occurs", async () => { + const error = new Error("test error"); + networkService.timeRequest = sinon.stub().throws(error); + try { + await networkService.requestHttp({ + data: { url: "http://test.com", _id: "123" }, + }); + } catch (error) { + expect(error).to.exist; + expect(error.method).to.equal("requestHttp"); + } + }); }); describe("requestPagespeed", () => { @@ -147,6 +183,18 @@ describe("Network Service", () => { expect(pagespeedResult.status).to.be.false; expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR); }); + it("should throw an error if pagespeed cannot resolve", async () => { + const error = new Error("test error"); + networkService.timeRequest = sinon.stub().throws(error); + try { + await networkService.requestPagespeed({ + data: { url: "http://test.com", _id: "123" }, + }); + } catch (error) { + expect(error).to.exist; + expect(error.method).to.equal("requestPagespeed"); + } + }); }); describe("requestHardware", () => { @@ -201,6 +249,99 @@ describe("Network Service", () => { expect(httpResult.status).to.be.false; expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); }); + it("should throw an error if hardware cannot resolve", async () => { + const error = new Error("test error"); + networkService.timeRequest = sinon.stub().throws(error); + try { + await networkService.requestHardware({ + data: { url: "http://test.com", _id: "123" }, + }); + } catch (error) { + expect(error).to.exist; + expect(error.method).to.equal("requestHardware"); + } + }); + }); + + describe("requestDocker", () => { + it("should return a response object if docker successful", async () => { + const job = { data: { url: "http://test.com", _id: "123", type: "docker" } }; + const dockerResult = await networkService.requestDocker(job); + expect(dockerResult.monitorId).to.equal("123"); + expect(dockerResult.type).to.equal("docker"); + expect(dockerResult.responseTime).to.be.a("number"); + expect(dockerResult.status).to.be.true; + }); + + it("should return a response object with status false if container not running", async () => { + Docker = class { + listContainers = sinon.stub().resolves([ + { + Names: ["/my_container"], + Id: "abc123", + }, + ]); + getContainer = sinon.stub().returns({ + inspect: sinon.stub().resolves({ State: { Status: "stopped" } }), + }); + }; + networkService = new NetworkService(axios, ping, logger, http, Docker); + const job = { data: { url: "abc123", _id: "123", type: "docker" } }; + const dockerResult = await networkService.requestDocker(job); + expect(dockerResult.status).to.be.false; + expect(dockerResult.code).to.equal(200); + }); + + it("should handle an error when fetching the container", async () => { + Docker = class { + listContainers = sinon.stub().resolves([ + { + Names: ["/my_container"], + Id: "abc123", + }, + ]); + getContainer = sinon.stub().returns({ + inspect: sinon.stub().throws(new Error("test error")), + }); + }; + networkService = new NetworkService(axios, ping, logger, http, Docker); + const job = { data: { url: "abc123", _id: "123", type: "docker" } }; + const dockerResult = await networkService.requestDocker(job); + expect(dockerResult.status).to.be.false; + expect(dockerResult.code).to.equal(networkService.NETWORK_ERROR); + }); + + it("should throw an error if operations fail", async () => { + Docker = class { + listContainers = sinon.stub().resolves([ + { + Names: ["/my_container"], + Id: "abc123", + }, + ]); + getContainer = sinon.stub().throws(new Error("test error")); + }; + networkService = new NetworkService(axios, ping, logger, http, Docker); + const job = { data: { url: "abc123", _id: "123", type: "docker" } }; + try { + await networkService.requestDocker(job); + } catch (error) { + expect(error.message).to.equal("test error"); + } + }); + it("should throw an error if no matching images found", async () => { + Docker = class { + listContainers = sinon.stub().resolves([]); + getContainer = sinon.stub().throws(new Error("test error")); + }; + networkService = new NetworkService(axios, ping, logger, http, Docker); + const job = { data: { url: "abc123", _id: "123", type: "docker" } }; + try { + await networkService.requestDocker(job); + } catch (error) { + expect(error.message).to.equal(errorMessages.DOCKER_NOT_FOUND); + } + }); }); describe("getStatus", () => { @@ -209,39 +350,81 @@ describe("Network Service", () => { networkService.requestHttp = sinon.stub(); networkService.requestPagespeed = sinon.stub(); networkService.requestHardware = sinon.stub(); + networkService.requestDocker = sinon.stub(); }); afterEach(() => { sinon.restore(); }); - it("should call requestPing if type is ping", () => { - networkService.getStatus({ data: { type: "ping" } }); + it("should call requestPing if type is ping", async () => { + await networkService.getStatus({ data: { type: "ping" } }); expect(networkService.requestPing.calledOnce).to.be.true; + expect(networkService.requestDocker.notCalled).to.be.true; expect(networkService.requestHttp.notCalled).to.be.true; expect(networkService.requestPagespeed.notCalled).to.be.true; }); - it("should call requestHttp if type is http", () => { - networkService.getStatus({ data: { type: "http" } }); + it("should call requestHttp if type is http", async () => { + await networkService.getStatus({ data: { type: "http" } }); expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestDocker.notCalled).to.be.true; expect(networkService.requestHttp.calledOnce).to.be.true; expect(networkService.requestPagespeed.notCalled).to.be.true; }); - it("should call requestPagespeed if type is pagespeed", () => { - networkService.getStatus({ data: { type: "pagespeed" } }); + it("should call requestPagespeed if type is pagespeed", async () => { + await networkService.getStatus({ data: { type: "pagespeed" } }); expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestDocker.notCalled).to.be.true; expect(networkService.requestHttp.notCalled).to.be.true; expect(networkService.requestPagespeed.calledOnce).to.be.true; }); - it("should call requestHardware if type is hardware", () => { - networkService.getStatus({ data: { type: "hardware" } }); + it("should call requestHardware if type is hardware", async () => { + await networkService.getStatus({ data: { type: "hardware" } }); expect(networkService.requestHardware.calledOnce).to.be.true; + expect(networkService.requestDocker.notCalled).to.be.true; expect(networkService.requestPing.notCalled).to.be.true; expect(networkService.requestPagespeed.notCalled).to.be.true; }); - it("should log an error if an unknown type is provided", () => { - networkService.getStatus({ data: { type: "unknown" } }); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.args[0][0].message).to.equal("Unsupported type: unknown"); + it("should call requestDocker if type is Docker", async () => { + await networkService.getStatus({ data: { type: "docker" } }); + expect(networkService.requestDocker.calledOnce).to.be.true; + expect(networkService.requestHardware.notCalled).to.be.true; + expect(networkService.requestPing.notCalled).to.be.true; + expect(networkService.requestPagespeed.notCalled).to.be.true; + }); + it("should throw an error if an unknown type is provided", async () => { + try { + await networkService.getStatus({ data: { type: "unknown" } }); + } catch (error) { + expect(error.service).to.equal("NetworkService"); + expect(error.method).to.equal("getStatus"); + expect(error.message).to.equal("Unsupported type: unknown"); + } + }); + it("should throw an error if job type is undefined", async () => { + try { + await networkService.getStatus({ data: { type: undefined } }); + } catch (error) { + expect(error.service).to.equal("NetworkService"); + expect(error.method).to.equal("getStatus"); + expect(error.message).to.equal("Unsupported type: unknown"); + } + }); + it("should throw an error if job is empty", async () => { + try { + await networkService.getStatus({}); + } catch (error) { + expect(error.method).to.equal("getStatus"); + expect(error.message).to.equal("Unsupported type: unknown"); + } + }); + it("should throw an error if job is null", async () => { + try { + await networkService.getStatus(null); + } catch (error) { + expect(error.service).to.equal("NetworkService"); + expect(error.method).to.equal("getStatus"); + expect(error.message).to.equal("Unsupported type: unknown"); + } }); }); }); diff --git a/Server/utils/messages.js b/Server/utils/messages.js index c75c617d2..2db427bd8 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -55,6 +55,10 @@ const errorMessages = { // Status Page Errors STATUS_PAGE_NOT_FOUND: "Status page not found", STATUS_PAGE_URL_NOT_UNIQUE: "Status page url must be unique", + + // Docker + DOCKER_FAIL: "Failed to fetch Docker container information", + DOCKER_NOT_FOUND: "Docker container not found", }; const successMessages = { @@ -123,6 +127,9 @@ const successMessages = { // Status Page STATUS_PAGE_BY_URL: "Got status page by url successfully", STATUS_PAGE_CREATE: "Status page created successfully", + + // Docker + DOCKER_SUCCESS: "Docker container status fetched successfully", }; export { errorMessages, successMessages };