Skip to content

Commit

Permalink
Merge pull request #1183 from c0derMo/master
Browse files Browse the repository at this point in the history
Adding option to monitor other docker containers
  • Loading branch information
louislam authored Aug 2, 2022
2 parents fb3fe17 + 1223b56 commit 70aa8fe
Show file tree
Hide file tree
Showing 16 changed files with 579 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ VPS is sponsored by Uptime Kuma sponsors on [Open Collective](https://opencollec

## ⭐ Features

* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server.
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers.
* Fancy, Reactive, Fast UI/UX.
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications).
* 20 second intervals.
Expand Down
18 changes: 18 additions & 0 deletions db/patch-add-docker-columns.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;

CREATE TABLE docker_host (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL,
docker_daemon VARCHAR(255),
docker_type VARCHAR(255),
name VARCHAR(255)
);

ALTER TABLE monitor
ADD docker_host INTEGER REFERENCES docker_host(id);

ALTER TABLE monitor
ADD docker_container VARCHAR(255);

COMMIT;
25 changes: 25 additions & 0 deletions server/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,35 @@ async function sendInfo(socket) {
});
}

/**
* Send list of docker hosts to client
* @param {Socket} socket Socket.io socket instance
* @returns {Promise<Bean[]>}
*/
async function sendDockerHostList(socket) {
const timeLogger = new TimeLogger();

let result = [];
let list = await R.find("docker_host", " user_id = ? ", [
socket.userID,
]);

for (let bean of list) {
result.push(bean.toJSON());
}

io.to(socket.userID).emit("dockerHostList", result);

timeLogger.print("Send Docker Host List");

return list;
}

module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
sendHeartbeatList,
sendProxyList,
sendInfo,
sendDockerHostList
};
1 change: 1 addition & 0 deletions server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Database {
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true,
"patch-add-docker-columns.sql": true,
"patch-status-page.sql": true,
"patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true,
Expand Down
106 changes: 106 additions & 0 deletions server/docker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");

class DockerHost {
/**
* Save a docker host
* @param {Object} dockerHost Docker host to save
* @param {?number} dockerHostID ID of the docker host to update
* @param {number} userID ID of the user who adds the docker host
* @returns {Promise<Bean>}
*/
static async save(dockerHost, dockerHostID, userID) {
let bean;

if (dockerHostID) {
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);

if (!bean) {
throw new Error("docker host not found");
}

} else {
bean = R.dispense("docker_host");
}

bean.user_id = userID;
bean.docker_daemon = dockerHost.dockerDaemon;
bean.docker_type = dockerHost.dockerType;
bean.name = dockerHost.name;

await R.store(bean);

return bean;
}

/**
* Delete a Docker host
* @param {number} dockerHostID ID of the Docker host to delete
* @param {number} userID ID of the user who created the Docker host
* @returns {Promise<void>}
*/
static async delete(dockerHostID, userID) {
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [ dockerHostID, userID ]);

if (!bean) {
throw new Error("docker host not found");
}

// Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET docker_host = null WHERE docker_host = ?", [ dockerHostID ]);

await R.trash(bean);
}

/**
* Fetches the amount of containers on the Docker host
* @param {Object} dockerHost Docker host to check for
* @returns {number} Total amount of containers on the host
*/
static async testDockerHost(dockerHost) {
const options = {
url: "/containers/json?all=true",
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: false,
}),
};

if (dockerHost.dockerType === "socket") {
options.socketPath = dockerHost.dockerDaemon;
} else if (dockerHost.dockerType === "tcp") {
options.baseURL = dockerHost.dockerDaemon;
}

let res = await axios.request(options);

if (Array.isArray(res.data)) {

if (res.data.length > 1) {

if ("ImageID" in res.data[0]) {
return res.data.length;
} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}

} else {
return res.data.length;
}

} else {
throw new Error("Invalid Docker response, is it Docker really a daemon?");
}

}
}

module.exports = {
DockerHost,
};
19 changes: 19 additions & 0 deletions server/model/docker_host.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { BeanModel } = require("redbean-node/dist/bean-model");

class DockerHost extends BeanModel {
/**
* Returns an object that ready to parse to JSON
* @returns {Object}
*/
toJSON() {
return {
id: this.id,
userID: this.user_id,
dockerDaemon: this.docker_daemon,
dockerType: this.docker_type,
name: this.name,
};
}
}

module.exports = DockerHost;
32 changes: 32 additions & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class Monitor extends BeanModel {
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
Expand Down Expand Up @@ -468,6 +471,35 @@ class Monitor extends BeanModel {
} else {
throw new Error("Server not found on Steam");
}
} else if (this.type === "docker") {
log.debug(`[${this.name}] Prepare Options for Axios`);

const dockerHost = await R.load("docker_host", this.docker_host);

const options = {
url: `/containers/${this.docker_container}/json`,
headers: {
"Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version,
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
};

if (dockerHost._dockerType === "socket") {
options.socketPath = dockerHost._dockerDaemon;
} else if (dockerHost._dockerType === "tcp") {
options.baseURL = dockerHost._dockerDaemon;
}

log.debug(`[${this.name}] Axios Request`);
let res = await axios.request(options);
if (res.data.State.Running) {
bean.status = UP;
bean.msg = "";
}
} else if (this.type === "mqtt") {
bean.msg = await mqttAsync(this.hostname, this.mqttTopic, this.mqttSuccessMessage, {
port: this.port,
Expand Down
7 changes: 6 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,14 @@ if (config.demoMode) {
}

// Must be after io instantiation
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList } = require("./client");
const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sendInfo, sendProxyList, sendDockerHostList } = require("./client");
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");

app.use(express.json());

Expand Down Expand Up @@ -680,6 +681,8 @@ let needSetup = false;
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken;
bean.docker_container = monitor.docker_container;
bean.docker_host = monitor.docker_host;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword;
Expand Down Expand Up @@ -1438,6 +1441,7 @@ let needSetup = false;
cloudflaredSocketHandler(socket);
databaseSocketHandler(socket);
proxySocketHandler(socket);
dockerSocketHandler(socket);

log.debug("server", "added all socket handlers");

Expand Down Expand Up @@ -1538,6 +1542,7 @@ async function afterLogin(socket, user) {
let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket);
sendProxyList(socket);
sendDockerHostList(socket);

await sleep(500);

Expand Down
79 changes: 79 additions & 0 deletions server/socket-handlers/docker-socket-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const { sendDockerHostList } = require("../client");
const { checkLogin } = require("../util-server");
const { DockerHost } = require("../docker");
const { log } = require("../../src/util");

/**
* Handlers for docker hosts
* @param {Socket} socket Socket.io instance
*/
module.exports.dockerSocketHandler = (socket) => {
socket.on("addDockerHost", async (dockerHost, dockerHostID, callback) => {
try {
checkLogin(socket);

let dockerHostBean = await DockerHost.save(dockerHost, dockerHostID, socket.userID);
await sendDockerHostList(socket);

callback({
ok: true,
msg: "Saved",
id: dockerHostBean.id,
});

} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});

socket.on("deleteDockerHost", async (dockerHostID, callback) => {
try {
checkLogin(socket);

await DockerHost.delete(dockerHostID, socket.userID);
await sendDockerHostList(socket);

callback({
ok: true,
msg: "Deleted",
});

} catch (e) {
callback({
ok: false,
msg: e.message,
});
}
});

socket.on("testDockerHost", async (dockerHost, callback) => {
try {
checkLogin(socket);

let amount = await DockerHost.testDockerHost(dockerHost);
let msg;

if (amount > 1) {
msg = "Connected Successfully. Amount of containers: " + amount;
} else {
msg = "Connected Successfully, but there are no containers?";
}

callback({
ok: true,
msg,
});

} catch (e) {
log.error("docker", e);

callback({
ok: false,
msg: e.message,
});
}
});
};
Loading

0 comments on commit 70aa8fe

Please sign in to comment.