purge ssh
This commit is contained in:
1 parent
074b94b061
commit
71fed33ecd
9 files changed
+13
-104
No files matched your search
@@ -634,7 +634,7 @@
|
||||
"option": {
|
||||
"dockerHost": {
|
||||
"label": "Docker host",
|
||||
"placeholder": "unix:///var/run/docker.sock or ssh://user@host"
|
||||
"placeholder": "unix:///var/run/docker.sock"
|
||||
},
|
||||
"host": {
|
||||
"label": "Host",
|
||||
@@ -716,7 +716,7 @@
|
||||
"http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard. Set an optional proxy server to connect through.",
|
||||
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
"docker": "Enter the Docker host to monitor: a local socket (unix:///var/run/docker.sock or /var/run/docker.sock) or a remote host over SSH (ssh://user@host, with an optional :port). Every container on the host is monitored.",
|
||||
"docker": "Enter the Docker socket to monitor, as unix:///var/run/docker.sock or an absolute path such as /var/run/docker.sock. Every container on the host is monitored.",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type. Some games require enabling server queries in their config file — check your game server's documentation. See the <gamedigLink>supported games list</gamedigLink> for protocol details.",
|
||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||
|
||||
@@ -120,22 +120,15 @@ const refineProxySelection = (body: { proxyMode?: string; proxyId?: string }, ct
|
||||
}
|
||||
};
|
||||
|
||||
const dockerUrlRegex = /^(?:ssh:\/\/[^@\s]+@[^\s/:@]+(?::\d{1,5})?\/?|unix:\/\/\/\S+|\/\S+)$/;
|
||||
const dockerUrlRegex = /^(?:unix:\/\/\/\S+|\/\S+)$/;
|
||||
|
||||
const refineDockerUrl = (data: { type?: string; url?: string; sshPrivateKey?: string }, ctx: z.RefinementCtx) => {
|
||||
const refineDockerUrl = (data: { type?: string; url?: string }, ctx: z.RefinementCtx) => {
|
||||
if (data.type !== "docker" || data.url === undefined) return;
|
||||
if (!dockerUrlRegex.test(data.url)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["url"],
|
||||
message: "Docker host must be ssh://user@host[:port], unix:///path, or an absolute socket path",
|
||||
});
|
||||
}
|
||||
if (data.url.startsWith("ssh://") && !data.sshPrivateKey) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["sshPrivateKey"],
|
||||
message: "SSH Docker hosts require a private key",
|
||||
message: "Docker host must be unix:///path or an absolute socket path",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -164,7 +157,6 @@ export const createMonitorBodyValidation = z
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
jsonPath: z.union([z.string(), z.literal("")]).optional(),
|
||||
expectedValue: z.union([z.string(), z.literal("")]).optional(),
|
||||
matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(),
|
||||
@@ -201,7 +193,6 @@ export const editMonitorBodyValidation = z
|
||||
tags: z.array(z.string()).optional(),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
ignoreTlsErrors: z.boolean().optional(),
|
||||
proxyMode: z.enum(ProxyModes).optional(),
|
||||
proxyId: proxyIdValidation,
|
||||
@@ -283,7 +274,6 @@ const importedMonitorSchema = z
|
||||
tags: z.array(z.string()).default([]),
|
||||
customUpCodes: z.array(httpStatusCode).default([]),
|
||||
secret: z.string().optional(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
cpuAlertThreshold: z.number().default(100),
|
||||
cpuAlertCounter: z.number().default(5),
|
||||
memoryAlertThreshold: z.number().default(100),
|
||||
@@ -379,7 +369,6 @@ export const monitorResponseSchema = z
|
||||
tags: z.array(z.string()),
|
||||
customUpCodes: z.array(httpStatusCode).optional(),
|
||||
secret: z.string().optional(),
|
||||
sshPrivateKey: z.string().optional(),
|
||||
cpuAlertThreshold: z.number(),
|
||||
memoryAlertThreshold: z.number(),
|
||||
diskAlertThreshold: z.number(),
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { ILogger } from "@/utils/logger.js";
|
||||
* The old docker provider interpreted monitor.url as a container name/ID and only
|
||||
* ever talked to the local socket, so every pre-existing docker monitor's host is
|
||||
* definitionally unix:///var/run/docker.sock. The container name is preserved in
|
||||
* the description. Urls already in host form (unix://, ssh://, or an absolute
|
||||
* socket path) are left untouched, which makes the migration idempotent.
|
||||
* the description. Urls already in host form (unix:// or an absolute socket
|
||||
* path) are left untouched, which makes the migration idempotent.
|
||||
*/
|
||||
export async function migrateDockerMonitorUrls(logger: ILogger): Promise<void> {
|
||||
const SERVICE_NAME = "Migration:MigrateDockerMonitorUrls";
|
||||
@@ -16,7 +16,7 @@ export async function migrateDockerMonitorUrls(logger: ILogger): Promise<void> {
|
||||
try {
|
||||
logger.info({ service: SERVICE_NAME, message: "Starting docker monitor url migration" });
|
||||
|
||||
const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^((unix|ssh):\/\/|\/)/ } }, [
|
||||
const result = await MonitorModel.updateMany({ type: "docker", url: { $not: /^(unix:\/\/|\/)/ } }, [
|
||||
{
|
||||
$set: {
|
||||
description: {
|
||||
|
||||
@@ -235,9 +235,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
secret: {
|
||||
type: String,
|
||||
},
|
||||
sshPrivateKey: {
|
||||
type: String,
|
||||
},
|
||||
cpuAlertThreshold: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
|
||||
@@ -436,7 +436,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
tags: tagIds,
|
||||
customUpCodes: doc.customUpCodes ?? [],
|
||||
secret: doc.secret ?? undefined,
|
||||
sshPrivateKey: doc.sshPrivateKey ?? undefined,
|
||||
cpuAlertThreshold: doc.cpuAlertThreshold,
|
||||
cpuAlertCounter: doc.cpuAlertCounter,
|
||||
memoryAlertThreshold: doc.memoryAlertThreshold,
|
||||
|
||||
@@ -92,7 +92,6 @@ export interface Monitor {
|
||||
tags: string[];
|
||||
customUpCodes: HttpStatusCode[];
|
||||
secret?: string;
|
||||
sshPrivateKey?: string;
|
||||
cpuAlertThreshold: number;
|
||||
cpuAlertCounter: number;
|
||||
memoryAlertThreshold: number;
|
||||
|
||||
@@ -63,31 +63,7 @@ export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
|
||||
return { socketPath };
|
||||
}
|
||||
if (url.startsWith("/")) return { socketPath: url };
|
||||
const match = url.match(/^ssh:\/\/(?:([^@\s]+)@)?([^\s/:@]+)(?::(\d{1,5}))?\/?$/);
|
||||
if (!match) throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" });
|
||||
const [, username, host, port] = match;
|
||||
if (!username)
|
||||
throw new AppError({
|
||||
message: "SSH Docker host URL requires a user: ssh://user@host",
|
||||
status: 422,
|
||||
service: SERVICE_NAME,
|
||||
method: "toDockerOptions",
|
||||
});
|
||||
|
||||
if (!monitor.sshPrivateKey)
|
||||
throw new AppError({
|
||||
message: "SSH Docker host requires a private key",
|
||||
status: 422,
|
||||
service: SERVICE_NAME,
|
||||
method: "toDockerOptions",
|
||||
});
|
||||
return {
|
||||
protocol: "ssh",
|
||||
host,
|
||||
port: port ? Number(port) : 22,
|
||||
username,
|
||||
sshOptions: { privateKey: monitor.sshPrivateKey },
|
||||
};
|
||||
throw new AppError({ message: `Invalid Docker host URL: ${url}`, status: 422, service: SERVICE_NAME, method: "toDockerOptions" });
|
||||
};
|
||||
|
||||
private async mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
|
||||
|
||||
@@ -9,8 +9,6 @@ import { DOCKER_LOG_TAIL_LINES } from "../../../../src/domain/docker/docker.type
|
||||
|
||||
// ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
({
|
||||
id: "mon-1",
|
||||
@@ -116,28 +114,6 @@ describe("DockerProvider", () => {
|
||||
|
||||
expect(DockerLib).toHaveBeenCalledWith({ socketPath: "/var/run/docker.sock" });
|
||||
});
|
||||
|
||||
it("parses ssh urls with a private key, defaulting to port 22", async () => {
|
||||
const { provider, DockerLib } = setup();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "ssh://deploy@prod-swarm-01.internal", sshPrivateKey: PRIVATE_KEY }));
|
||||
|
||||
expect(DockerLib).toHaveBeenCalledWith({
|
||||
protocol: "ssh",
|
||||
host: "prod-swarm-01.internal",
|
||||
port: 22,
|
||||
username: "deploy",
|
||||
sshOptions: { privateKey: PRIVATE_KEY },
|
||||
});
|
||||
});
|
||||
|
||||
it("parses an explicit ssh port", async () => {
|
||||
const { provider, DockerLib } = setup();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "ssh://deploy@host:2222", sshPrivateKey: PRIVATE_KEY }));
|
||||
|
||||
expect(DockerLib).toHaveBeenCalledWith(expect.objectContaining({ port: 2222 }));
|
||||
});
|
||||
});
|
||||
|
||||
// ── Fail-loudly url validation ───────────────────────────────────────
|
||||
@@ -151,7 +127,7 @@ describe("DockerProvider", () => {
|
||||
["a container name (old semantics)", "my-container", "Invalid Docker host URL"],
|
||||
["tcp engine urls (unsupported)", "tcp://host:2375", "Invalid Docker host URL"],
|
||||
["https engine urls (unsupported)", "https://host", "Invalid Docker host URL"],
|
||||
["ssh without a user", "ssh://host", "SSH Docker host URL requires a user: ssh://user@host"],
|
||||
["ssh engine urls (unsupported)", "ssh://deploy@host", "Invalid Docker host URL"],
|
||||
];
|
||||
|
||||
it.each(invalidUrls)("throws AppError for %s and never constructs a client", async (_label, url, message) => {
|
||||
@@ -161,15 +137,6 @@ describe("DockerProvider", () => {
|
||||
expect(DockerLib).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws AppError for an ssh url with no private key", async () => {
|
||||
const { provider, DockerLib } = setup();
|
||||
|
||||
await expect(provider.handle(makeMonitor({ url: "ssh://deploy@host", sshPrivateKey: undefined }))).rejects.toThrow(
|
||||
"SSH Docker host requires a private key"
|
||||
);
|
||||
expect(DockerLib).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves the toDockerOptions AppError through handle's catch", async () => {
|
||||
const { provider } = setup();
|
||||
|
||||
|
||||
@@ -608,7 +608,6 @@ describe("monitorValidation — Docker host url", () => {
|
||||
type: "docker" as const,
|
||||
url: "unix:///var/run/docker.sock",
|
||||
};
|
||||
const PRIVATE_KEY = "-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
describe("createMonitorBodyValidation", () => {
|
||||
it("accepts the default unix socket url", () => {
|
||||
@@ -621,32 +620,15 @@ describe("monitorValidation — Docker host url", () => {
|
||||
expect(parsed.url).toBe("/run/docker.sock");
|
||||
});
|
||||
|
||||
it("accepts an ssh url with a private key and retains the key", () => {
|
||||
for (const url of ["ssh://deploy@host", "ssh://deploy@host:2222"]) {
|
||||
const parsed = createMonitorBodyValidation.parse({ ...baseDockerBody, url, sshPrivateKey: PRIVATE_KEY });
|
||||
expect(parsed.sshPrivateKey).toBe(PRIVATE_KEY);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects container names and unsupported engine urls", () => {
|
||||
for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "unix://relative/path", ""]) {
|
||||
for (const badUrl of ["my-container", "tcp://host:2375", "https://host", "ssh://deploy@host", "unix://relative/path", ""]) {
|
||||
expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: badUrl })).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an ssh url without a user", () => {
|
||||
expect(() => createMonitorBodyValidation.parse({ ...baseDockerBody, url: "ssh://host", sshPrivateKey: PRIVATE_KEY })).toThrow();
|
||||
});
|
||||
|
||||
it("rejects an ssh url without a private key, attributing the issue to sshPrivateKey", () => {
|
||||
const result = createMonitorBodyValidation.safeParse({ ...baseDockerBody, url: "ssh://deploy@host" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues.some((issue) => issue.path.includes("sshPrivateKey"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not apply docker url rules to other monitor types", () => {
|
||||
const parsed = createMonitorBodyValidation.parse({ name: "HTTP check", type: "http", url: "https://example.com" });
|
||||
expect(parsed.sshPrivateKey).toBeUndefined();
|
||||
expect(parsed.url).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -660,7 +642,7 @@ describe("monitorValidation — Docker host url", () => {
|
||||
expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "my-container" })).toThrow();
|
||||
});
|
||||
|
||||
it("rejects a docker edit switching to ssh without a key", () => {
|
||||
it("rejects a docker edit with an ssh url", () => {
|
||||
expect(() => editMonitorBodyValidation.parse({ type: "docker", url: "ssh://deploy@host" })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user