mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-08-04 13:47:37 +02:00
Basic Fallback for Validation, Parsing errors
This commit is contained in:
parent
5d524e8060
commit
065df720d1
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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", {})
|
||||||
|
@ -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})
|
||||||
|
@ -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. ***")
|
||||||
|
@ -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
|
||||||
|
@ -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})")
|
||||||
|
|
||||||
|
@ -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}")
|
||||||
|
@ -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 />} />
|
||||||
|
@ -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">
|
||||||
|
@ -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 (shouldn’t 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 />;
|
||||||
}
|
}
|
||||||
|
11
web/src/components/settings/ConfigProtection.tsx
Normal file
11
web/src/components/settings/ConfigProtection.tsx
Normal 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 />;
|
||||||
|
}
|
13
web/src/hooks/use-config-validator.ts
Normal file
13
web/src/hooks/use-config-validator.ts
Normal 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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user