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:
Josh Hawkins 2024-12-18 14:10:32 -06:00 committed by GitHub
parent c4af9205c2
commit b4d0add62b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 5 deletions

View File

@ -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("*************************************************************")

View File

@ -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
@ -183,7 +186,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=(
@ -194,13 +196,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,