From b4d0add62b7e1e3c8434524dffad27ea455ac10d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:10:32 -0600 Subject: [PATCH] Preserve line numbers in config validation (#15584) * use ruamel to parse and preserve line numbers for config validation * maintain exception for non validation errors * fix types * include input in log messages --- frigate/__main__.py | 49 +++++++++++++++++++++++++++++++++++--- frigate/api/app.py | 57 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/frigate/__main__.py b/frigate/__main__.py index b086d33b3..468fe9f98 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -3,12 +3,15 @@ import faulthandler import signal import sys import threading +from typing import Union +import ruamel.yaml from pydantic import ValidationError from frigate.app import FrigateApp from frigate.config import FrigateConfig from frigate.log import setup_logging +from frigate.util.config import find_config_file def main() -> None: @@ -42,10 +45,50 @@ def main() -> None: print("*************************************************************") print("*************************************************************") print("*** Config Validation Errors ***") - print("*************************************************************") + print("*************************************************************\n") + # Attempt to get the original config file for line number tracking + config_path = find_config_file() + with open(config_path, "r") as f: + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(f) + for error in e.errors(): - location = ".".join(str(item) for item in error["loc"]) - print(f"{location}: {error['msg']}") + error_path = error["loc"] + + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key: Union[int, str] = ( + int(part) if isinstance(part, str) and part.isdigit() else part + ) + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + if isinstance(key, int): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception as traverse_error: + print(f"Could not determine exact line number: {traverse_error}") + + print(f"Line # : {line_number}") + print(f"Key : {' -> '.join(map(str, error_path))}") + print(f"Value : {error.get('input','-')}") + print(f"Message : {error.get('msg', error.get('type', 'Unknown'))}\n") + print("*************************************************************") print("*** End Config Validation Errors ***") print("*************************************************************") diff --git a/frigate/api/app.py b/frigate/api/app.py index 57efdeb15..b1e798ead 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -7,15 +7,18 @@ import os import traceback from datetime import datetime, timedelta from functools import reduce +from io import StringIO from typing import Any, Optional import requests +import ruamel.yaml from fastapi import APIRouter, Body, Path, Request, Response from fastapi.encoders import jsonable_encoder from fastapi.params import Depends from fastapi.responses import JSONResponse, PlainTextResponse from markupsafe import escape from peewee import operator +from pydantic import ValidationError from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import AppConfigSetBody @@ -183,7 +186,6 @@ def config_raw(): @router.post("/config/save") def config_save(save_option: str, body: Any = Body(media_type="text/plain")): new_config = body.decode() - if not new_config: return JSONResponse( content=( @@ -194,13 +196,64 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")): # Validate the config schema try: + # Use ruamel to parse and preserve line numbers + yaml_config = ruamel.yaml.YAML() + yaml_config.preserve_quotes = True + full_config = yaml_config.load(StringIO(new_config)) + FrigateConfig.parse_yaml(new_config) + + except ValidationError as e: + error_message = [] + + for error in e.errors(): + error_path = error["loc"] + current = full_config + line_number = "Unknown" + last_line_number = "Unknown" + + try: + for i, part in enumerate(error_path): + key = int(part) if part.isdigit() else part + + if isinstance(current, ruamel.yaml.comments.CommentedMap): + current = current[key] + elif isinstance(current, list): + current = current[key] + + if hasattr(current, "lc"): + last_line_number = current.lc.line + + if i == len(error_path) - 1: + if hasattr(current, "lc"): + line_number = current.lc.line + else: + line_number = last_line_number + + except Exception: + line_number = "Unable to determine" + + error_message.append( + f"Line {line_number}: {' -> '.join(map(str, error_path))} - {error.get('msg', error.get('type', 'Unknown'))}" + ) + + return JSONResponse( + content=( + { + "success": False, + "message": "Your configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n" + + "\n".join(error_message), + } + ), + status_code=400, + ) + except Exception: return JSONResponse( content=( { "success": False, - "message": f"\nConfig Error:\n\n{escape(str(traceback.format_exc()))}", + "message": f"\nYour configuration is invalid.\nSee the official documentation at docs.frigate.video.\n\n{escape(str(traceback.format_exc()))}", } ), status_code=400,