Basic Fallback for Validation, Parsing errors

This commit is contained in:
Alan 2025-03-14 20:28:13 +00:00
parent 5d524e8060
commit 065df720d1
13 changed files with 206 additions and 50 deletions

View File

@ -34,6 +34,14 @@ services:
- ./config:/config - ./config:/config
- ./debug:/media/frigate - ./debug:/media/frigate
- /dev/bus/usb:/dev/bus/usb - /dev/bus/usb:/dev/bus/usb
ports:
- "8971:8971"
- "5000:5000"
- "5001:5001"
- "5173:5173"
- "8554:8554"
- "8555:8555"
mqtt: mqtt:
container_name: mqtt container_name: mqtt
image: eclipse-mosquitto:1.6 image: eclipse-mosquitto:1.6

View File

@ -3,6 +3,7 @@ import os
import sys import sys
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml.scanner import ScannerError
sys.path.insert(0, "/opt/frigate") sys.path.insert(0, "/opt/frigate")
from frigate.const import ( from frigate.const import (
@ -29,7 +30,7 @@ try:
config: dict[str, any] = yaml.load(raw_config) config: dict[str, any] = yaml.load(raw_config)
elif config_file.endswith(".json"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except (FileNotFoundError, ScannerError):
config: dict[str, any] = {} config: dict[str, any] = {}
path = config.get("ffmpeg", {}).get("path", "default") path = config.get("ffmpeg", {}).get("path", "default")

View File

@ -6,6 +6,7 @@ import sys
from pathlib import Path from pathlib import Path
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml.scanner import ScannerError
sys.path.insert(0, "/opt/frigate") sys.path.insert(0, "/opt/frigate")
from frigate.const import ( from frigate.const import (
@ -44,7 +45,7 @@ try:
config: dict[str, any] = yaml.load(raw_config) config: dict[str, any] = yaml.load(raw_config)
elif config_file.endswith(".json"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except (FileNotFoundError, ScannerError):
config: dict[str, any] = {} config: dict[str, any] = {}
go2rtc_config: dict[str, any] = config.get("go2rtc", {}) go2rtc_config: dict[str, any] = config.get("go2rtc", {})

View File

@ -2,8 +2,8 @@
import json import json
import os import os
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml.scanner import ScannerError
yaml = YAML() yaml = YAML()
@ -22,7 +22,7 @@ try:
config: dict[str, any] = yaml.load(raw_config) config: dict[str, any] = yaml.load(raw_config)
elif config_file.endswith(".json"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except (FileNotFoundError, ScannerError):
config: dict[str, any] = {} config: dict[str, any] = {}
tls_config: dict[str, any] = config.get("tls", {"enabled": True}) tls_config: dict[str, any] = config.get("tls", {"enabled": True})

View File

@ -7,12 +7,28 @@ from typing import Union
import ruamel.yaml import ruamel.yaml
from pydantic import ValidationError from pydantic import ValidationError
from ruamel.yaml.scanner import ScannerError
from frigate.app import FrigateApp from frigate.app import FrigateApp
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.log import setup_logging from frigate.log import setup_logging
from frigate.util.config import find_config_file from frigate.util.config import find_config_file
minimal_config = {
"mqtt": {"enabled": "false"},
"environment_vars": {
"INVALID_CONFIG": "true",
},
"cameras": {
"null": {
"ffmpeg": {
"inputs": [
{"path": "/dev/null"}
]
}
}
},
}
def main() -> None: def main() -> None:
faulthandler.enable() faulthandler.enable()
@ -47,6 +63,8 @@ def main() -> None:
print("*** Config Validation Errors ***") print("*** Config Validation Errors ***")
print("*************************************************************\n") print("*************************************************************\n")
# Attempt to get the original config file for line number tracking # Attempt to get the original config file for line number tracking
if e.__class__ == ValidationError:
config_path = find_config_file() config_path = find_config_file()
with open(config_path, "r") as f: with open(config_path, "r") as f:
yaml_config = ruamel.yaml.YAML() yaml_config = ruamel.yaml.YAML()
@ -89,11 +107,15 @@ def main() -> None:
print(f"Key : {' -> '.join(map(str, error_path))}") print(f"Key : {' -> '.join(map(str, error_path))}")
print(f"Value : {error.get('input', '-')}") print(f"Value : {error.get('input', '-')}")
print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n")
else:
print(f"Failed to parse config: {e}")
print("*************************************************************") print("*************************************************************")
print("*** End Config Validation Errors ***") print("*** End Config Validation Errors ***")
print("*************************************************************") print("*************************************************************")
sys.exit(1)
FrigateApp(FrigateConfig(**minimal_config)).start_config_editor()
sys.exit(0)
if args.validate_config: if args.validate_config:
print("*************************************************************") print("*************************************************************")
print("*** Your config file is valid. ***") print("*** Your config file is valid. ***")

View File

@ -130,3 +130,42 @@ def create_fastapi_app(
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
return app return app
def create_config_editor_app(
frigate_config: FrigateConfig,
database: SqliteQueueDatabase,
) -> FastAPI:
app = FastAPI(
debug=True,
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
)
app.add_middleware(
middleware.ContextMiddleware,
plugins=(plugins.ForwardedForPlugin(),),
)
@app.on_event("startup")
async def startup():
logger.info("FastAPI started")
# Rate limiter (used for login endpoint)
if frigate_config.auth.failed_login_rate_limit is None:
limiter.enabled = False
else:
auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Routes
app.include_router(auth.router)
app.include_router(main_app.router)
app.frigate_config = frigate_config
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
return app

View File

@ -15,7 +15,7 @@ from playhouse.sqlite_ext import SqliteExtDatabase
import frigate.util as util import frigate.util as util
from frigate.api.auth import hash_password from frigate.api.auth import hash_password
from frigate.api.fastapi_app import create_fastapi_app from frigate.api.fastapi_app import create_config_editor_app, create_fastapi_app
from frigate.camera import CameraMetrics, PTZMetrics from frigate.camera import CameraMetrics, PTZMetrics
from frigate.comms.base_communicator import Communicator from frigate.comms.base_communicator import Communicator
from frigate.comms.config_updater import ConfigPublisher from frigate.comms.config_updater import ConfigPublisher
@ -624,6 +624,37 @@ class FrigateApp:
logger.info("********************************************************") logger.info("********************************************************")
logger.info("********************************************************") logger.info("********************************************************")
def start_config_editor(self) -> None:
logger.info(f"Starting Frigate in Config Editor Mode ({VERSION})")
self.ensure_dirs()
self.init_database()
self.bind_database()
self.init_auth()
def stop_config_editor() -> None:
logger.info("Stopping...")
os._exit(os.EX_OK)
try:
uvicorn.run(
create_config_editor_app(
self.config,
self.db,
),
host="127.0.0.1",
port=5001,
log_level="error",
)
finally:
stop_config_editor()
def start(self) -> None: def start(self) -> None:
logger.info(f"Starting Frigate ({VERSION})") logger.info(f"Starting Frigate ({VERSION})")

View File

@ -7,6 +7,7 @@ import shutil
from typing import Optional, Union from typing import Optional, Union
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml.scanner import ScannerError
from frigate.const import CONFIG_DIR, EXPORT_DIR from frigate.const import CONFIG_DIR, EXPORT_DIR
from frigate.util.services import get_video_properties from frigate.util.services import get_video_properties
@ -37,7 +38,11 @@ def migrate_frigate_config(config_file: str):
yaml = YAML() yaml = YAML()
yaml.indent(mapping=2, sequence=4, offset=2) yaml.indent(mapping=2, sequence=4, offset=2)
with open(config_file, "r") as f: with open(config_file, "r") as f:
try:
config: dict[str, dict[str, any]] = yaml.load(f) config: dict[str, dict[str, any]] = yaml.load(f)
except ScannerError:
logger.debug(f"Failed to parse config at {config_file}")
return
if config is None: if config is None:
logger.error(f"Failed to load config at {config_file}") logger.error(f"Failed to load config at {config_file}")

View File

@ -46,6 +46,17 @@ function App() {
> >
<Suspense> <Suspense>
<Routes> <Routes>
<Route
element={
<ProtectedRoute
requiredRoles={["admin"]}
configGuard={false}
/>
}
>
<Route path="/config" element={<ConfigEditor />} />
</Route>
<Route <Route
element={ element={
<ProtectedRoute requiredRoles={["viewer", "admin"]} /> <ProtectedRoute requiredRoles={["viewer", "admin"]} />
@ -61,7 +72,6 @@ function App() {
element={<ProtectedRoute requiredRoles={["admin"]} />} element={<ProtectedRoute requiredRoles={["admin"]} />}
> >
<Route path="/system" element={<System />} /> <Route path="/system" element={<System />} />
<Route path="/config" element={<ConfigEditor />} />
<Route path="/logs" element={<Logs />} /> <Route path="/logs" element={<Logs />} />
<Route path="/faces" element={<FaceLibrary />} /> <Route path="/faces" element={<FaceLibrary />} />
<Route path="/playground" element={<UIPlayground />} /> <Route path="/playground" element={<UIPlayground />} />

View File

@ -9,6 +9,7 @@ import { FaCheck } from "react-icons/fa";
import { IoIosWarning } from "react-icons/io"; import { IoIosWarning } from "react-icons/io";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useConfigValidator } from "@/hooks/use-config-validator"; // <-- new import
export default function Statusbar() { export default function Statusbar() {
const { messages, addMessage, clearMessages } = useContext( const { messages, addMessage, clearMessages } = useContext(
@ -28,6 +29,7 @@ export default function Statusbar() {
}, [stats]); }, [stats]);
const { potentialProblems } = useStats(stats); const { potentialProblems } = useStats(stats);
const invalidConfig = useConfigValidator(); // <-- use the hook
useEffect(() => { useEffect(() => {
clearMessages("stats"); clearMessages("stats");
@ -40,7 +42,10 @@ export default function Statusbar() {
problem.relevantLink, problem.relevantLink,
); );
}); });
}, [potentialProblems, addMessage, clearMessages]); if (invalidConfig) {
addMessage("stats", "Invalid config - fix config only", "text-danger");
}
}, [potentialProblems, invalidConfig, addMessage, clearMessages]);
const { payload: reindexState } = useEmbeddingsReindexProgress(); const { payload: reindexState } = useEmbeddingsReindexProgress();
@ -60,7 +65,11 @@ export default function Statusbar() {
}, [reindexState, addMessage, clearMessages]); }, [reindexState, addMessage, clearMessages]);
return ( return (
<div className="absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight bg-background_alt px-4 dark:text-secondary-foreground"> <div
className={`absolute bottom-0 left-0 right-0 z-10 flex h-8 w-full items-center justify-between border-t border-secondary-highlight ${
invalidConfig ? "bg-red-500 bg-opacity-75" : "bg-background_alt"
} px-4 dark:text-secondary-foreground`}
>
<div className="flex h-full items-center gap-2"> <div className="flex h-full items-center gap-2">
{cpuPercent && ( {cpuPercent && (
<Link to="/system#general"> <Link to="/system#general">

View File

@ -2,13 +2,19 @@ import { useContext } from "react";
import { Navigate, Outlet } from "react-router-dom"; import { Navigate, Outlet } from "react-router-dom";
import { AuthContext } from "@/context/auth-context"; import { AuthContext } from "@/context/auth-context";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { useConfigValidator } from "@/hooks/use-config-validator";
interface ProtectedRouteProps {
requiredRoles: ("admin" | "viewer")[];
configGuard?: boolean;
}
export default function ProtectedRoute({ export default function ProtectedRoute({
requiredRoles, requiredRoles,
}: { configGuard = true,
requiredRoles: ("admin" | "viewer")[]; }: ProtectedRouteProps) {
}) {
const { auth } = useContext(AuthContext); const { auth } = useContext(AuthContext);
const invalidConfig = useConfigValidator(); // Use the hook to check config validity
if (auth.isLoading) { if (auth.isLoading) {
return ( return (
@ -16,18 +22,14 @@ export default function ProtectedRoute({
); );
} }
// Unauthenticated mode
if (!auth.isAuthenticated) { if (!auth.isAuthenticated) {
return <Outlet />; return <Outlet />;
} }
// Authenticated mode (8971): require login
if (!auth.user) { if (!auth.user) {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
// If role is null (shouldnt happen if isAuthenticated, but type safety), fallback
// though isAuthenticated should catch this
if (auth.user.role === null) { if (auth.user.role === null) {
return <Outlet />; return <Outlet />;
} }
@ -36,5 +38,9 @@ export default function ProtectedRoute({
return <Navigate to="/unauthorized" replace />; return <Navigate to="/unauthorized" replace />;
} }
if (configGuard && invalidConfig) {
return <Navigate to="/config" replace />;
}
return <Outlet />; return <Outlet />;
} }

View File

@ -0,0 +1,11 @@
import { useConfigValidator } from "@/hooks/use-config-validator";
import { Navigate, Outlet, useLocation } from "react-router-dom";
export default function ConfigProtection() {
const invalidConfig = useConfigValidator();
const location = useLocation();
if (invalidConfig && location.pathname !== "/config") {
return <Navigate to="/config" replace />;
}
return <Outlet />;
}

View File

@ -0,0 +1,13 @@
import { useMemo } from "react";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
export function useConfigValidator() {
const { data: config } = useSWR<FrigateConfig>("config");
const invalidConfig = useMemo(() => {
return config?.environment_vars?.INVALID_CONFIG;
}, [config]);
return invalidConfig;
}