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
							
								
									0aabf9a24f
								
							
						
					
					
						commit
						69a2f948f2
					
				@ -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
 | 
				
			||||||
@ -185,7 +188,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=(
 | 
				
			||||||
@ -196,13 +198,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