Docker Hosts are now a table & have their own dialog

This commit is contained in:
c0derMo 2022-07-22 15:47:04 +00:00
parent ac449ec1c2
commit 0d098b0958
10 changed files with 385 additions and 31 deletions

View File

@ -1,13 +1,18 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. -- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION; 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 ALTER TABLE monitor
ADD docker_daemon VARCHAR(255); ADD docker_host INTEGER REFERENCES docker_host(id);
ALTER TABLE monitor ALTER TABLE monitor
ADD docker_container VARCHAR(255); ADD docker_container VARCHAR(255);
ALTER TABLE monitor
ADD docker_type VARCHAR(255);
COMMIT; COMMIT;

View File

@ -122,10 +122,30 @@ async function sendInfo(socket) {
}); });
} }
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.export());
}
io.to(socket.userID).emit("dockerHostList", result);
timeLogger.print("Send Docker Host List");
return list;
}
module.exports = { module.exports = {
sendNotificationList, sendNotificationList,
sendImportantHeartbeatList, sendImportantHeartbeatList,
sendHeartbeatList, sendHeartbeatList,
sendProxyList, sendProxyList,
sendInfo, sendInfo,
sendDockerHostList
}; };

67
server/docker.js Normal file
View File

@ -0,0 +1,67 @@
const axios = require("axios");
const { R } = require("redbean-node");
const version = require("../package.json").version;
const https = require("https");
class DockerHost {
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.docker_daemon;
bean.docker_type = dockerHost.docker_type;
bean.name = dockerHost.name;
await R.store(bean);
return bean;
}
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");
}
await R.trash(bean);
}
static async getAmountContainer(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.docker_type === "socket") {
options.socketPath = dockerHost.docker_daemon;
} else if (dockerHost.docker_type === "tcp") {
options.baseURL = dockerHost.docker_daemon;
}
let res = await axios.request(options);
return res.data.length;
}
}
module.exports = {
DockerHost,
}

View File

@ -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,
daemon: this._dockerDaemon,
type: this._dockerType,
name: this._name,
}
}
}
module.exports = DockerHost;

View File

@ -89,8 +89,7 @@ class Monitor extends BeanModel {
dns_last_result: this.dns_last_result, dns_last_result: this.dns_last_result,
pushToken: this.pushToken, pushToken: this.pushToken,
docker_container: this.docker_container, docker_container: this.docker_container,
docker_daemon: this.docker_daemon, docker_host: this.docker_host,
docker_type: this.docker_type,
proxyId: this.proxy_id, proxyId: this.proxy_id,
notificationIDList, notificationIDList,
tags: tags, tags: tags,
@ -471,6 +470,8 @@ class Monitor extends BeanModel {
} else if (this.type === "docker") { } else if (this.type === "docker") {
log.debug(`[${this.name}] Prepare Options for Axios`); log.debug(`[${this.name}] Prepare Options for Axios`);
const docker_host = await R.load("docker_host", this.docker_host);
const options = { const options = {
url: `/containers/${this.docker_container}/json`, url: `/containers/${this.docker_container}/json`,
headers: { headers: {
@ -483,10 +484,10 @@ class Monitor extends BeanModel {
}), }),
}; };
if (this.docker_type === "socket") { if (docker_host._dockerType === "socket") {
options.socketPath = this.docker_daemon; options.socketPath = docker_host._dockerDaemon;
} else if (this.docker_type === "tcp") { } else if (docker_host._dockerType === "tcp") {
options.baseURL = this.docker_daemon; options.baseURL = docker_host._dockerDaemon;
} }
log.debug(`[${this.name}] Axios Request`); log.debug(`[${this.name}] Axios Request`);

View File

@ -118,13 +118,14 @@ if (config.demoMode) {
} }
// Must be after io instantiation // 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 { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const databaseSocketHandler = require("./socket-handlers/database-socket-handler"); const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
const TwoFA = require("./2fa"); const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page"); const StatusPage = require("./model/status_page");
const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler"); const { cloudflaredSocketHandler, autoStart: cloudflaredAutoStart, stop: cloudflaredStop } = require("./socket-handlers/cloudflared-socket-handler");
const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler"); const { proxySocketHandler } = require("./socket-handlers/proxy-socket-handler");
const { dockerSocketHandler } = require("./socket-handlers/docker-socket-handler");
app.use(express.json()); app.use(express.json());
@ -665,8 +666,7 @@ let needSetup = false;
bean.dns_resolve_server = monitor.dns_resolve_server; bean.dns_resolve_server = monitor.dns_resolve_server;
bean.pushToken = monitor.pushToken; bean.pushToken = monitor.pushToken;
bean.docker_container = monitor.docker_container; bean.docker_container = monitor.docker_container;
bean.docker_daemon = monitor.docker_daemon; bean.docker_host = monitor.docker_host;
bean.docker_type = monitor.docker_type;
bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null; bean.proxyId = Number.isInteger(monitor.proxyId) ? monitor.proxyId : null;
bean.mqttUsername = monitor.mqttUsername; bean.mqttUsername = monitor.mqttUsername;
bean.mqttPassword = monitor.mqttPassword; bean.mqttPassword = monitor.mqttPassword;
@ -1425,6 +1425,7 @@ let needSetup = false;
cloudflaredSocketHandler(socket); cloudflaredSocketHandler(socket);
databaseSocketHandler(socket); databaseSocketHandler(socket);
proxySocketHandler(socket); proxySocketHandler(socket);
dockerSocketHandler(socket);
log.debug("server", "added all socket handlers"); log.debug("server", "added all socket handlers");
@ -1525,6 +1526,7 @@ async function afterLogin(socket, user) {
let monitorList = await server.sendMonitorList(socket); let monitorList = await server.sendMonitorList(socket);
sendNotificationList(socket); sendNotificationList(socket);
sendProxyList(socket); sendProxyList(socket);
sendDockerHostList(socket);
await sleep(500); await sleep(500);

View File

@ -0,0 +1,67 @@
const { sendDockerHostList } = require("../client");
const { checkLogin } = require("../util-server");
const { DockerHost } = require("../docker");
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.getAmountContainer(dockerHost);
callback({
ok: true,
msg: "Amount of containers: " + amount,
});
} catch (e) {
console.error(e);
callback({
ok: false,
msg: e.message,
})
}
})
}

View File

@ -0,0 +1,160 @@
<template>
<form @submit.prevent="submit">
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 id="exampleModalLabel" class="modal-title">
{{ $t("Setup Docker Host") }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
</div>
<div class="modal-body">
<div class="mb-3">
<label for="docker-name" class="form-label">{{ $t("Friendly Name") }}</label>
<input id="docker-name" v-model="dockerHost.name" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label for="docker-type" class="form-label">{{ $t("Connection Type") }}</label>
<select id="docker-type" v-model="dockerHost.dockerType" class="form-select">
<option v-for="type in connectionTypes" :key="type" :value="type">{{ $t(type) }}</option>
</select>
</div>
<div class="mb-3">
<label for="docker-daemon" class="form-label">{{ $t("Docker Daemon") }}</label>
<input id="docker-daemon" v-model="dockerHost.dockerDaemon" type="text" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
{{ $t("Delete") }}
</button>
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
{{ $t("Test") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="processing">
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
{{ $t("Save") }}
</button>
</div>
</div>
</div>
</div>
</form>
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
{{ $t("deleteDockerHostMsg") }}
</Confirm>
</template>
<script lang="ts">
import { Modal } from "bootstrap";
import Confirm from "./Confirm.vue";
export default {
components: {
Confirm,
},
props: {},
emits: [ "added" ],
data() {
return {
model: null,
processing: false,
id: null,
connectionTypes: ["socket", "tcp"],
dockerHost: {
name: "",
dockerDaemon: "",
dockerType: "",
// Do not set default value here, please scroll to show()
}
};
},
mounted() {
this.modal = new Modal(this.$refs.modal);
},
methods: {
deleteConfirm() {
this.modal.hide();
this.$refs.confirmDelete.show();
},
show(dockerHostID) {
if (dockerHostID) {
this.id = dockerHostID;
for (let n of this.$root.dockerHostList) {
if (n.id === dockerHostID) {
this.dockerHost = n;
break;
}
}
} else {
this.id = null;
this.dockerHost = {
name: "",
dockerType: "socket",
dockerDaemon: "/var/run/docker.sock",
};
}
this.modal.show();
},
submit() {
this.processing = true;
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
// Emit added event, doesn't emit edit.
if (! this.id) {
this.$emit("added", res.id);
}
}
});
},
test() {
this.processing = true;
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
this.$root.toastRes(res);
this.processing = false;
});
},
deleteDockerHost() {
this.processing = true;
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
this.$root.toastRes(res);
this.processing = false;
if (res.ok) {
this.modal.hide();
}
});
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.dark {
.modal-dialog .form-text, .modal-dialog p {
color: $dark-font-color;
}
}
</style>

View File

@ -39,6 +39,7 @@ export default {
uptimeList: { }, uptimeList: { },
tlsInfoList: {}, tlsInfoList: {},
notificationList: [], notificationList: [],
dockerHostList: [],
statusPageListLoaded: false, statusPageListLoaded: false,
statusPageList: [], statusPageList: [],
proxyList: [], proxyList: [],
@ -141,6 +142,10 @@ export default {
}); });
}); });
socket.on("dockerHostList", (data) => {
this.dockerHostList = data;
})
socket.on("heartbeat", (data) => { socket.on("heartbeat", (data) => {
if (! (data.monitorID in this.heartbeatList)) { if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = []; this.heartbeatList[data.monitorID] = [];

View File

@ -148,25 +148,25 @@
<input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required> <input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required>
</div> </div>
<!-- Docker Connection Type --> <!-- Docker Host -->
<!-- For Docker Type --> <!-- For Docker Type -->
<div v-if="monitor.type === 'docker'" class="my-3"> <div v-if="monitor.type === 'docker'" class="my-3">
<label for="docker_type" class="form-label">{{ $t("Docker Type") }}</label> <h2 class="mb-2">{{ $t("Docker Host") }}</h2>
<select id="docker_type" v-model="monitor.docker_type" class="form-select"> <p v-if="$root.dockerHostList.length === 0">
<option value="socket"> {{ $t("Not available, please setup.") }}
{{ $t("docker_socket") }} </p>
</option>
<option value="tcp">
{{ $t("docker_tcp") }}
</option>
</select>
</div>
<!-- Docker Daemon --> <div class="mb-3">
<!-- For Docker Type --> <label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
<div v-if="monitor.type === 'docker'" class="my-3"> <select id="docket-host" v-model="monitor.docker_host" class="form-select">
<label for="docker_daemon" class="form-label">{{ $t("Docker Daemon") }}</label> <option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
<input id="docker_daemon" v-model="monitor.docker_daemon" type="text" class="form-control" required> </select>
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
</div>
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
{{ $t("Setup Docker Host") }}
</button>
</div> </div>
<!-- MQTT --> <!-- MQTT -->
@ -446,6 +446,7 @@
</form> </form>
<NotificationDialog ref="notificationDialog" @added="addedNotification" /> <NotificationDialog ref="notificationDialog" @added="addedNotification" />
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
<ProxyDialog ref="proxyDialog" @added="addedProxy" /> <ProxyDialog ref="proxyDialog" @added="addedProxy" />
</div> </div>
</transition> </transition>
@ -456,6 +457,7 @@ import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import CopyableInput from "../components/CopyableInput.vue"; import CopyableInput from "../components/CopyableInput.vue";
import NotificationDialog from "../components/NotificationDialog.vue"; import NotificationDialog from "../components/NotificationDialog.vue";
import DockerHostDialog from "../components/DockerHostDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue"; import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue"; import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev } from "../util.ts"; import { genSecret, isDev } from "../util.ts";
@ -467,6 +469,7 @@ export default {
ProxyDialog, ProxyDialog,
CopyableInput, CopyableInput,
NotificationDialog, NotificationDialog,
DockerHostDialog,
TagsManager, TagsManager,
VueMultiselect, VueMultiselect,
}, },
@ -625,8 +628,7 @@ export default {
dns_resolve_type: "A", dns_resolve_type: "A",
dns_resolve_server: "1.1.1.1", dns_resolve_server: "1.1.1.1",
docker_container: "", docker_container: "",
docker_daemon: "/var/run/docker.sock", docker_host: null,
docker_type: "socket",
proxyId: null, proxyId: null,
mqttUsername: "", mqttUsername: "",
mqttPassword: "", mqttPassword: "",
@ -740,6 +742,12 @@ export default {
addedProxy(id) { addedProxy(id) {
this.monitor.proxyId = id; this.monitor.proxyId = id;
}, },
// Added a Docker Host Event
// Enable it if the Docker Host is added in EditMonitor.vue
addedDockerHost(id) {
this.monitor.docker_host = id;
}
}, },
}; };
</script> </script>