summaryrefslogtreecommitdiffstats
path: root/server/docker.js
blob: ee6051dfae773fda81442c0bedd9946d2bdcaab0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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,
};