mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
chore(duplication): share redis runtime helpers (#5490)
This commit is contained in:
46
src/app/api/local/redis/redisRuntime.ts
Normal file
46
src/app/api/local/redis/redisRuntime.ts
Normal 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() };
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
tests/unit/local-redis-runtime.test.ts
Normal file
55
tests/unit/local-redis-runtime.test.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user