summaryrefslogtreecommitdiffstats
path: root/server/docker.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/docker.js')
-rw-r--r--server/docker.js179
1 files changed, 179 insertions, 0 deletions
diff --git a/server/docker.js b/server/docker.js
new file mode 100644
index 0000000..ee6051d
--- /dev/null
+++ b/server/docker.js
@@ -0,0 +1,179 @@
+const axios = require("axios");
+const { R } = require("redbean-node");
+const https = require("https");
+const fs = require("fs");
+const path = require("path");
+const Database = require("./database");
+const { axiosAbortSignal } = require("./util-server");
+
+class DockerHost {
+
+ static CertificateFileNameCA = "ca.pem";
+ static CertificateFileNameCert = "cert.pem";
+ static CertificateFileNameKey = "key.pem";
+
+ /**
+ * 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>} Updated docker host
+ */
+ 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 {Promise<number>} Total amount of containers on the host
+ */
+ static async testDockerHost(dockerHost) {
+ const options = {
+ url: "/containers/json?all=true",
+ timeout: 5000,
+ headers: {
+ "Accept": "*/*",
+ },
+ signal: axiosAbortSignal(6000),
+ };
+
+ if (dockerHost.dockerType === "socket") {
+ options.socketPath = dockerHost.dockerDaemon;
+ } else if (dockerHost.dockerType === "tcp") {
+ options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
+ options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
+ }
+
+ try {
+ 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?");
+ }
+ } catch (e) {
+ if (e.code === "ECONNABORTED" || e.name === "CanceledError") {
+ throw new Error("Connection to Docker daemon timed out.");
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Since axios 0.27.X, it does not accept `tcp://` protocol.
+ * Change it to `http://` on the fly in order to fix it. (https://github.com/louislam/uptime-kuma/issues/2165)
+ * @param {any} url URL to fix
+ * @returns {any} URL with tcp:// replaced by http://
+ */
+ static patchDockerURL(url) {
+ if (typeof url === "string") {
+ // Replace the first occurrence only with g
+ return url.replace(/tcp:\/\//g, "http://");
+ }
+ return url;
+ }
+
+ /**
+ * Returns HTTPS agent options with client side TLS parameters if certificate files
+ * for the given host are available under a predefined directory path.
+ *
+ * The base path where certificates are looked for can be set with the
+ * 'DOCKER_TLS_DIR_PATH' environmental variable or defaults to 'data/docker-tls/'.
+ *
+ * If a directory in this path exists with a name matching the FQDN of the docker host
+ * (e.g. the FQDN of 'https://example.com:2376' is 'example.com' so the directory
+ * 'data/docker-tls/example.com/' would be searched for certificate files),
+ * then 'ca.pem', 'key.pem' and 'cert.pem' files are included in the agent options.
+ * File names can also be overridden via 'DOCKER_TLS_FILE_NAME_(CA|KEY|CERT)'.
+ * @param {string} dockerType i.e. "tcp" or "socket"
+ * @param {string} url The docker host URL rewritten to https://
+ * @returns {object} HTTP agent options
+ */
+ static getHttpsAgentOptions(dockerType, url) {
+ let baseOptions = {
+ maxCachedSessions: 0,
+ rejectUnauthorized: true
+ };
+ let certOptions = {};
+
+ let dirName = (new URL(url)).hostname;
+
+ let caPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCA);
+ let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
+ let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
+
+ if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
+ let ca = fs.readFileSync(caPath);
+ let key = fs.readFileSync(keyPath);
+ let cert = fs.readFileSync(certPath);
+ certOptions = {
+ ca,
+ key,
+ cert
+ };
+ }
+
+ return {
+ ...baseOptions,
+ ...certOptions
+ };
+ }
+}
+
+module.exports = {
+ DockerHost,
+};