Route GitLab stats and health checks through a configurable proxy
This commit is contained in:
@@ -54,6 +54,7 @@ automatically. Non-secret settings go directly in `docker-compose.yml`:
|
|||||||
| `OPENWEBUI_TOKEN` | Open WebUI API key — enables the prompt line + model list (**set in `.env`**) |
|
| `OPENWEBUI_TOKEN` | Open WebUI API key — enables the prompt line + model list (**set in `.env`**) |
|
||||||
| `GITEA_TOKEN` | Gitea API token — private repo stats (public repos work without it) |
|
| `GITEA_TOKEN` | Gitea API token — private repo stats (public repos work without it) |
|
||||||
| `GITLAB_TOKEN` | GitLab API token — private project stats (public projects work without it) |
|
| `GITLAB_TOKEN` | GitLab API token — private project stats (public projects work without it) |
|
||||||
|
| `GITLAB_PROXY` | Forward proxy (HTTP CONNECT) for all GitLab requests — git stats **and** the online badge (**set in `.env`**). Change the URL when the proxy stack moves; unset = direct connection. With the proxy on the docker host use `http://host.docker.internal:8118` (the container resolves `host.docker.internal` via `extra_hosts`)
|
||||||
| `GITEA_URL` / `GITLAB_URL` / `NETBIRD_URL` / `OPENWEBUI_URL` | Override the default service URLs |
|
| `GITEA_URL` / `GITLAB_URL` / `NETBIRD_URL` / `OPENWEBUI_URL` | Override the default service URLs |
|
||||||
| `GPU_STATS_URL` | GPU stats upstream (default: the `gpu-stats` compose service) |
|
| `GPU_STATS_URL` | GPU stats upstream (default: the `gpu-stats` compose service) |
|
||||||
| `OLLAMA_URL` | Ollama instance for loaded-model stats (default: `http://100.103.83.12:11435`) |
|
| `OLLAMA_URL` | Ollama instance for loaded-model stats (default: `http://100.103.83.12:11435`) |
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Outbound proxy support for the container backend scripts.
|
||||||
|
//
|
||||||
|
// Node's core http/https modules ignore proxy env vars, so requests that
|
||||||
|
// should go through a forward proxy are tunneled manually via HTTP CONNECT.
|
||||||
|
// Currently used for GitLab access (see GITLAB_PROXY below).
|
||||||
|
//
|
||||||
|
// Configure in .env / docker-compose.yml (no rebuild needed, container
|
||||||
|
// restart is enough):
|
||||||
|
// GITLAB_PROXY=http://127.0.0.1:8118 (updater run on the host)
|
||||||
|
// GITLAB_PROXY=http://host.docker.internal:8118 (proxy on the docker host)
|
||||||
|
// GITLAB_PROXY=http://10.0.0.5:8118 (proxy moved elsewhere)
|
||||||
|
// Empty/unset disables proxying (direct connection).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const http = require("http");
|
||||||
|
const tls = require("tls");
|
||||||
|
const { URL } = require("url");
|
||||||
|
|
||||||
|
const GITLAB_PROXY = process.env.GITLAB_PROXY || "";
|
||||||
|
const GITLAB_HOST = (() => {
|
||||||
|
try {
|
||||||
|
return new URL(process.env.GITLAB_URL || "https://gitlab.ixsol.wien").hostname;
|
||||||
|
} catch {
|
||||||
|
return "gitlab.ixsol.wien";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Returns the proxy URL to use for a given target URL, or null for direct.
|
||||||
|
function proxyFor(url) {
|
||||||
|
if (!GITLAB_PROXY) return null;
|
||||||
|
try {
|
||||||
|
if (new URL(url).hostname === GITLAB_HOST) return GITLAB_PROXY;
|
||||||
|
} catch {
|
||||||
|
// unparseable URL — no proxy
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens a CONNECT tunnel through the proxy and resolves to a socket usable
|
||||||
|
// as the connection for a request to targetUrl: a TLS socket for https
|
||||||
|
// targets, the raw tunnel socket otherwise.
|
||||||
|
function connectThroughProxy(proxyUrl, targetUrl, timeoutMs, tlsOptions = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const target = new URL(targetUrl);
|
||||||
|
const proxy = new URL(proxyUrl);
|
||||||
|
const port = target.port || (target.protocol === "https:" ? "443" : "80");
|
||||||
|
const req = http.request({
|
||||||
|
host: proxy.hostname,
|
||||||
|
port: proxy.port || 80,
|
||||||
|
method: "CONNECT",
|
||||||
|
path: `${target.hostname}:${port}`,
|
||||||
|
headers: { Host: `${target.hostname}:${port}` },
|
||||||
|
timeout: timeoutMs || 10000,
|
||||||
|
});
|
||||||
|
req.on("connect", (res, socket) => {
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error(`Proxy CONNECT to ${target.hostname} failed: HTTP ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(
|
||||||
|
target.protocol === "https:"
|
||||||
|
? tls.connect({ socket, servername: target.hostname, ...tlsOptions })
|
||||||
|
: socket
|
||||||
|
);
|
||||||
|
});
|
||||||
|
req.on("timeout", () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error(`Proxy CONNECT to ${target.hostname} timed out`));
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request options for client.request() that route the request through the
|
||||||
|
// proxy: https via the CONNECT tunnel (caller passes the socket returned by
|
||||||
|
// connectThroughProxy), plain http via an absolute-form request URI.
|
||||||
|
function proxiedRequestOptions(proxyUrl, targetUrl, socket) {
|
||||||
|
const u = new URL(targetUrl);
|
||||||
|
if (u.protocol === "https:") {
|
||||||
|
return { agent: false, createConnection: () => socket };
|
||||||
|
}
|
||||||
|
const p = new URL(proxyUrl);
|
||||||
|
return {
|
||||||
|
host: p.hostname,
|
||||||
|
port: p.port || 80,
|
||||||
|
path: u.href,
|
||||||
|
headers: { Host: u.host },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { proxyFor, connectThroughProxy, proxiedRequestOptions };
|
||||||
+24
-8
@@ -13,6 +13,7 @@ const https = require("https");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
|
||||||
|
|
||||||
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/data.json";
|
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/data.json";
|
||||||
const TZ = process.env.TZ || "UTC";
|
const TZ = process.env.TZ || "UTC";
|
||||||
@@ -41,18 +42,33 @@ const GITLAB_TOKEN = process.env.GITLAB_TOKEN || "";
|
|||||||
const OPENWEBUI_URL = process.env.OPENWEBUI_URL || "http://100.103.83.12:3000";
|
const OPENWEBUI_URL = process.env.OPENWEBUI_URL || "http://100.103.83.12:3000";
|
||||||
const OPENWEBUI_TOKEN = process.env.OPENWEBUI_TOKEN || "";
|
const OPENWEBUI_TOKEN = process.env.OPENWEBUI_TOKEN || "";
|
||||||
|
|
||||||
function request(url, options = {}) {
|
// Central outbound request helper: honors the outbound proxy (GITLAB_PROXY)
|
||||||
|
// for the configured host — see api/proxy.js.
|
||||||
|
async function request(url, options = {}) {
|
||||||
|
const u = new URL(url);
|
||||||
|
const client = u.protocol === "https:" ? https : http;
|
||||||
|
const requestOptions = {
|
||||||
|
method: options.method || "GET",
|
||||||
|
headers: options.headers || {},
|
||||||
|
timeout: options.timeout || 15000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxy = proxyFor(url);
|
||||||
|
if (proxy) {
|
||||||
|
// https is tunneled through the proxy via CONNECT; plain http uses an
|
||||||
|
// absolute-form request URI addressed to the proxy.
|
||||||
|
let socket = null;
|
||||||
|
if (u.protocol === "https:") {
|
||||||
|
socket = await connectThroughProxy(proxy, url, requestOptions.timeout);
|
||||||
|
}
|
||||||
|
Object.assign(requestOptions, proxiedRequestOptions(proxy, url, socket));
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const u = new URL(url);
|
|
||||||
const client = u.protocol === "https:" ? https : http;
|
|
||||||
const req = client.request(
|
const req = client.request(
|
||||||
u,
|
u,
|
||||||
{
|
requestOptions,
|
||||||
method: options.method || "GET",
|
|
||||||
headers: options.headers || {},
|
|
||||||
timeout: options.timeout || 15000,
|
|
||||||
},
|
|
||||||
(res) => {
|
(res) => {
|
||||||
let body = "";
|
let body = "";
|
||||||
res.setEncoding("utf8");
|
res.setEncoding("utf8");
|
||||||
|
|||||||
+48
-31
@@ -15,6 +15,7 @@ const https = require("https");
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { URL } = require("url");
|
const { URL } = require("url");
|
||||||
|
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
|
||||||
|
|
||||||
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/health.json";
|
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/health.json";
|
||||||
const TIMEOUT = parseInt(process.env.HEALTH_TIMEOUT || "5000", 10);
|
const TIMEOUT = parseInt(process.env.HEALTH_TIMEOUT || "5000", 10);
|
||||||
@@ -51,40 +52,56 @@ function checkUrl(url) {
|
|||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
let req;
|
(async () => {
|
||||||
try {
|
let req;
|
||||||
const u = new URL(url);
|
try {
|
||||||
const client = u.protocol === "https:" ? https : http;
|
const u = new URL(url);
|
||||||
req = client.request(
|
const client = u.protocol === "https:" ? https : http;
|
||||||
u,
|
const options = {
|
||||||
{
|
|
||||||
method: "GET",
|
method: "GET",
|
||||||
agent: u.protocol === "https:" ? httpsAgent : httpAgent,
|
|
||||||
timeout: TIMEOUT,
|
timeout: TIMEOUT,
|
||||||
},
|
};
|
||||||
(res) => {
|
|
||||||
const code = res.statusCode;
|
|
||||||
res.destroy(); // response headers are all we need
|
|
||||||
done({
|
|
||||||
status: code >= 500 ? "error" : "online",
|
|
||||||
code,
|
|
||||||
latencyMs: Date.now() - started,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.on("timeout", () => {
|
// Honor the outbound proxy (GITLAB_PROXY) so the GitLab badge checks
|
||||||
req.destroy();
|
// the same path the git stats actually take (see api/proxy.js).
|
||||||
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
|
const proxy = proxyFor(url);
|
||||||
});
|
if (proxy) {
|
||||||
req.on("error", (e) => {
|
let socket = null;
|
||||||
done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
if (u.protocol === "https:") {
|
||||||
});
|
socket = await connectThroughProxy(proxy, url, TIMEOUT, { rejectUnauthorized: false });
|
||||||
req.end();
|
}
|
||||||
|
Object.assign(options, proxiedRequestOptions(proxy, url, socket));
|
||||||
|
} else {
|
||||||
|
options.agent = u.protocol === "https:" ? httpsAgent : httpAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
req = client.request(
|
||||||
|
u,
|
||||||
|
options,
|
||||||
|
(res) => {
|
||||||
|
const code = res.statusCode;
|
||||||
|
res.destroy(); // response headers are all we need
|
||||||
|
done({
|
||||||
|
status: code >= 500 ? "error" : "online",
|
||||||
|
code,
|
||||||
|
latencyMs: Date.now() - started,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.on("timeout", () => {
|
||||||
|
req.destroy();
|
||||||
|
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
|
||||||
|
});
|
||||||
|
req.on("error", (e) => {
|
||||||
|
done({ status: "offline", code: null, latencyMs: null, error: e.message });
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ services:
|
|||||||
# Uncomment and fill in to enable private git stats.
|
# Uncomment and fill in to enable private git stats.
|
||||||
# - GITLAB_TOKEN=
|
# - GITLAB_TOKEN=
|
||||||
# - NETBIRD_TOKEN=
|
# - NETBIRD_TOKEN=
|
||||||
|
# Forward proxy for GitLab API access + health checks (api/proxy.js).
|
||||||
|
# Set the URL in .env — change it there when the proxy stack moves
|
||||||
|
# (unset = direct connection). With the proxy on the docker host:
|
||||||
|
# GITLAB_PROXY=http://host.docker.internal:8118
|
||||||
|
- GITLAB_PROXY=${GITLAB_PROXY:-}
|
||||||
# GPU stats upstream; defaults to the gpu-stats service below.
|
# GPU stats upstream; defaults to the gpu-stats service below.
|
||||||
# To run gpu_stats.py on the host instead, use:
|
# To run gpu_stats.py on the host instead, use:
|
||||||
# http://host.docker.internal:9101/stats
|
# http://host.docker.internal:9101/stats
|
||||||
@@ -29,6 +34,10 @@ services:
|
|||||||
# - GPU_STATS_URL=http://gpu-stats:9101/stats
|
# - GPU_STATS_URL=http://gpu-stats:9101/stats
|
||||||
# Ollama instance for the loaded-model stats in the GPU widget.
|
# Ollama instance for the loaded-model stats in the GPU widget.
|
||||||
# - OLLAMA_URL=http://100.103.83.12:11435
|
# - OLLAMA_URL=http://100.103.83.12:11435
|
||||||
|
extra_hosts:
|
||||||
|
# Alias for the docker host, so container services can reach host
|
||||||
|
# processes (e.g. the proxy behind GITLAB_PROXY) by a stable name.
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
# Persists the Outlook OAuth refresh token across container updates.
|
# Persists the Outlook OAuth refresh token across container updates.
|
||||||
- outlook-tokens:/opt/dashboard/tokens
|
- outlook-tokens:/opt/dashboard/tokens
|
||||||
|
|||||||
Reference in New Issue
Block a user