chore(duplication): share redis runtime helpers (#5490)

This commit is contained in:
Jan Leon
2026-06-30 02:58:35 +02:00
committed by GitHub
parent 15eeaa31c5
commit 3bc0d5feeb
4 changed files with 141 additions and 54 deletions

View File

@@ -0,0 +1,46 @@
import { NextResponse } from "next/server";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
export const REDIS_CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis";
export const RUNTIME_PREFERENCE = ["podman", "docker"] as const;
type ExecFileAsync = (
file: string,
args: readonly string[],
options: { timeout: number }
) => Promise<{ stdout: string; stderr: string }>;
const execFileAsync = promisify(execFile) as ExecFileAsync;
export async function detectRedisContainerRuntime(
runCommand: ExecFileAsync = execFileAsync
): Promise<string | null> {
for (const candidate of RUNTIME_PREFERENCE) {
try {
await runCommand(candidate, ["--version"], { timeout: 3000 });
return candidate;
} catch {
// try next
}
}
return null;
}
export function redisRuntimeUnavailableResponse() {
return NextResponse.json(
{ ok: false, error: "No container runtime (podman or docker) found on PATH" },
{ status: 503 }
);
}
export async function runRedisRuntimeCommand(
runtime: string,
args: readonly string[],
timeout: number,
runCommand: ExecFileAsync = execFileAsync
) {
const { stdout, stderr } = await runCommand(runtime, args, { timeout });
return { stdout: stdout.trim(), stderr: stderr.trim() };
}

View File

@@ -1,42 +1,27 @@
import { NextResponse } from "next/server";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { isLocalRequestAllowed } from "@/lib/security/localEndpoints";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
const execFileAsync = promisify(execFile);
import {
REDIS_CONTAINER_NAME,
detectRedisContainerRuntime,
redisRuntimeUnavailableResponse,
runRedisRuntimeCommand,
} from "../redisRuntime";
const CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis";
const HOST_PORT = process.env.OMNIROUTE_REDIS_HOST_PORT || "6379";
const IMAGE = process.env.OMNIROUTE_REDIS_IMAGE || "docker.io/redis:7-alpine";
const RUNTIME_PREFERENCE = ["podman", "docker"];
async function detectRuntime(): Promise<string | null> {
for (const candidate of RUNTIME_PREFERENCE) {
try {
await execFileAsync(candidate, ["--version"], { timeout: 3000 });
return candidate;
} catch {
// try next
}
}
return null;
}
export async function POST() {
const guard = isLocalRequestAllowed();
if (!guard.allowed) {
return NextResponse.json({ error: guard.reason }, { status: 403 });
}
const runtime = await detectRuntime();
const runtime = await detectRedisContainerRuntime();
if (!runtime) {
return NextResponse.json(
{ ok: false, error: "No container runtime (podman or docker) found on PATH" },
{ status: 503 }
);
return redisRuntimeUnavailableResponse();
}
try {
@@ -48,15 +33,22 @@ export async function POST() {
"run",
"-d",
"--name",
CONTAINER_NAME,
REDIS_CONTAINER_NAME,
"-p",
`${HOST_PORT}:6379`,
"--restart",
"unless-stopped",
IMAGE,
];
const { stdout, stderr } = await execFileAsync(runtime, args, { timeout: 30_000 });
return NextResponse.json({ ok: true, runtime, name: CONTAINER_NAME, port: HOST_PORT, stdout: stdout.trim(), stderr: stderr.trim() });
const { stdout, stderr } = await runRedisRuntimeCommand(runtime, args, 30_000);
return NextResponse.json({
ok: true,
runtime,
name: REDIS_CONTAINER_NAME,
port: HOST_PORT,
stdout,
stderr,
});
} catch (err) {
// Hard Rule #12: never put a raw execFile error (command line + paths) in the body.
return NextResponse.json(
@@ -64,4 +56,4 @@ export async function POST() {
{ status: 500 }
);
}
}
}

View File

@@ -1,27 +1,14 @@
import { NextResponse } from "next/server";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { isLocalRequestAllowed } from "@/lib/security/localEndpoints";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
const execFileAsync = promisify(execFile);
const CONTAINER_NAME = process.env.OMNIROUTE_REDIS_CONTAINER_NAME || "omniroute-redis";
const RUNTIME_PREFERENCE = ["podman", "docker"];
async function detectRuntime(): Promise<string | null> {
for (const candidate of RUNTIME_PREFERENCE) {
try {
await execFileAsync(candidate, ["--version"], { timeout: 3000 });
return candidate;
} catch {
// try next
}
}
return null;
}
import {
REDIS_CONTAINER_NAME,
detectRedisContainerRuntime,
redisRuntimeUnavailableResponse,
runRedisRuntimeCommand,
} from "../redisRuntime";
export async function POST() {
const guard = isLocalRequestAllowed();
@@ -29,17 +16,24 @@ export async function POST() {
return NextResponse.json({ error: guard.reason }, { status: 403 });
}
const runtime = await detectRuntime();
const runtime = await detectRedisContainerRuntime();
if (!runtime) {
return NextResponse.json(
{ ok: false, error: "No container runtime (podman or docker) found on PATH" },
{ status: 503 }
);
return redisRuntimeUnavailableResponse();
}
try {
const { stdout, stderr } = await execFileAsync(runtime, ["stop", CONTAINER_NAME], { timeout: 15_000 });
return NextResponse.json({ ok: true, runtime, name: CONTAINER_NAME, stdout: stdout.trim(), stderr: stderr.trim() });
const { stdout, stderr } = await runRedisRuntimeCommand(
runtime,
["stop", REDIS_CONTAINER_NAME],
15_000
);
return NextResponse.json({
ok: true,
runtime,
name: REDIS_CONTAINER_NAME,
stdout,
stderr,
});
} catch (err) {
const rawMessage = err instanceof Error ? err.message : String(err);
// exit code != 0 from `stop` typically means "not running" — surface that as ok=false but don't 500
@@ -52,4 +46,4 @@ export async function POST() {
{ status: 500 }
);
}
}
}

View File

@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
REDIS_CONTAINER_NAME,
detectRedisContainerRuntime,
redisRuntimeUnavailableResponse,
runRedisRuntimeCommand,
} from "../../src/app/api/local/redis/redisRuntime.ts";
test("detectRedisContainerRuntime returns the first available runtime", async () => {
const calls: string[] = [];
const runtime = await detectRedisContainerRuntime(async (file) => {
calls.push(file);
if (file === "podman") {
throw new Error("missing");
}
return { stdout: "Docker version 1\n", stderr: "" };
});
assert.equal(runtime, "docker");
assert.deepEqual(calls, ["podman", "docker"]);
});
test("detectRedisContainerRuntime returns null when no runtime is available", async () => {
const runtime = await detectRedisContainerRuntime(async () => {
throw new Error("missing");
});
assert.equal(runtime, null);
});
test("runRedisRuntimeCommand trims command output", async () => {
const result = await runRedisRuntimeCommand(
"docker",
["stop", REDIS_CONTAINER_NAME],
15_000,
async () => ({
stdout: " stopped \n",
stderr: " warning \n",
})
);
assert.deepEqual(result, { stdout: "stopped", stderr: "warning" });
});
test("redisRuntimeUnavailableResponse preserves the route error shape", async () => {
const response = redisRuntimeUnavailableResponse();
assert.equal(response.status, 503);
assert.deepEqual(await response.json(), {
ok: false,
error: "No container runtime (podman or docker) found on PATH",
});
});