180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
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,
|
|
};
|