mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	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
This commit is contained in:
		
							parent
							
								
									877b7b2910
								
							
						
					
					
						commit
						edab4efa42
					
				| @ -3,12 +3,15 @@ import faulthandler | |||||||
| import signal | import signal | ||||||
| import sys | import sys | ||||||
| import threading | import threading | ||||||
|  | from typing import Union | ||||||
| 
 | 
 | ||||||
|  | import ruamel.yaml | ||||||
| from pydantic import ValidationError | from pydantic import ValidationError | ||||||
| 
 | 
 | ||||||
| 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def main() -> None: | def main() -> None: | ||||||
| @ -42,10 +45,50 @@ def main() -> None: | |||||||
|         print("*************************************************************") |         print("*************************************************************") | ||||||
|         print("*************************************************************") |         print("*************************************************************") | ||||||
|         print("***    Config Validation Errors                           ***") |         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(): |         for error in e.errors(): | ||||||
|             location = ".".join(str(item) for item in error["loc"]) |             error_path = error["loc"] | ||||||
|             print(f"{location}: {error['msg']}") | 
 | ||||||
|  |             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("*************************************************************") | ||||||
|         print("***    End Config Validation Errors                       ***") |         print("***    End Config Validation Errors                       ***") | ||||||
|         print("*************************************************************") |         print("*************************************************************") | ||||||
|  | |||||||
| @ -7,15 +7,18 @@ import os | |||||||
| import traceback | import traceback | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from functools import reduce | from functools import reduce | ||||||
|  | from io import StringIO | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
| 
 | 
 | ||||||
| import requests | import requests | ||||||
|  | import ruamel.yaml | ||||||
| from fastapi import APIRouter, Body, Path, Request, Response | from fastapi import APIRouter, Body, Path, Request, Response | ||||||
| from fastapi.encoders import jsonable_encoder | from fastapi.encoders import jsonable_encoder | ||||||
| from fastapi.params import Depends | from fastapi.params import Depends | ||||||
| from fastapi.responses import JSONResponse, PlainTextResponse | from fastapi.responses import JSONResponse, PlainTextResponse | ||||||
| from markupsafe import escape | from markupsafe import escape | ||||||
| from peewee import operator | from peewee import operator | ||||||
|  | from pydantic import ValidationError | ||||||
| 
 | 
 | ||||||
| from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters | from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters | ||||||
| from frigate.api.defs.request.app_body import AppConfigSetBody | from frigate.api.defs.request.app_body import AppConfigSetBody | ||||||
| @ -186,7 +189,6 @@ def config_raw(): | |||||||
| @router.post("/config/save") | @router.post("/config/save") | ||||||
| def config_save(save_option: str, body: Any = Body(media_type="text/plain")): | def config_save(save_option: str, body: Any = Body(media_type="text/plain")): | ||||||
|     new_config = body.decode() |     new_config = body.decode() | ||||||
| 
 |  | ||||||
|     if not new_config: |     if not new_config: | ||||||
|         return JSONResponse( |         return JSONResponse( | ||||||
|             content=( |             content=( | ||||||
| @ -197,13 +199,64 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")): | |||||||
| 
 | 
 | ||||||
|     # Validate the config schema |     # Validate the config schema | ||||||
|     try: |     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) |         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: |     except Exception: | ||||||
|         return JSONResponse( |         return JSONResponse( | ||||||
|             content=( |             content=( | ||||||
|                 { |                 { | ||||||
|                     "success": False, |                     "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, |             status_code=400, | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user