diff --git a/nac_validate/cli/main.py b/nac_validate/cli/main.py index 198c79e..fb19fd5 100644 --- a/nac_validate/cli/main.py +++ b/nac_validate/cli/main.py @@ -123,6 +123,16 @@ def version_callback(value: bool) -> None: ] +DisableYamllint = Annotated[ + bool, + typer.Option( + "--disable-yamllint", + help="Disable yamllint validation.", + envvar="NAC_VALIDATE_DISABLE_YAMLLINT", + ), +] + + Version = Annotated[ bool, typer.Option( @@ -142,13 +152,16 @@ def main( rules: Rules = DEFAULT_RULES, output: Output = None, non_strict: NonStrict = False, + disable_yamllint: DisableYamllint = False, version: Version = False, ) -> None: """A CLI tool to perform syntactic and semantic validation of YAML files.""" configure_logging(verbosity) try: - validator = nac_validate.validator.Validator(schema, rules) + validator = nac_validate.validator.Validator( + schema, rules, enable_yamllint=not disable_yamllint + ) validator.validate_syntax(paths, not non_strict) validator.validate_semantics(paths) if output: diff --git a/nac_validate/validator.py b/nac_validate/validator.py index 4bac414..063665d 100644 --- a/nac_validate/validator.py +++ b/nac_validate/validator.py @@ -12,6 +12,8 @@ from typing import Any import yamale +import yamllint.config +import yamllint.linter from nac_yaml.yaml import load_yaml_files, write_yaml_file from ruamel import yaml from yamale.yamale_error import YamaleError @@ -29,7 +31,10 @@ class Validator: - def __init__(self, schema_path: Path, rules_path: Path): + def __init__( + self, schema_path: Path, rules_path: Path, enable_yamllint: bool = False + ): + self.enable_yamllint = enable_yamllint self.data: dict[str, Any] | None = None self.schema = None if os.path.exists(schema_path): @@ -71,6 +76,39 @@ def __init__(self, schema_path: Path, rules_path: Path): f"Rules directory not found: {rules_path}" ) + def _run_yamllint(self, file_path: Path) -> None: + """Run yamllint validation on a file""" + if file_path.suffix not in [".yaml", ".yml"]: + return + + logger.debug(f"Running yamllint on {file_path}") + + try: + # NAC-specific yamllint configuration - only new-lines validation enabled + config_str = "{extends: default, rules: {anchors: disable, braces: disable, brackets: disable, colons: disable, commas: disable, comments: disable, comments-indentation: disable, document-end: disable, document-start: disable, empty-lines: disable, empty-values: disable, float-values: disable, hyphens: disable, indentation: disable, key-duplicates: disable, key-ordering: disable, line-length: disable, new-line-at-end-of-file: disable, new-lines: enable, octal-values: disable, quoted-strings: disable, trailing-spaces: disable, truthy: disable}}" + config = yamllint.config.YamlLintConfig(config_str) + + # Read file as bytes to preserve original line endings + with open(file_path, "rb") as f: + content = f.read() + + problems = yamllint.linter.run(content, config, file_path) + problem_list = list(problems) + + logger.debug(f"Yamllint found {len(problem_list)} problems") + + if problem_list: + # Log yamllint problems but don't block other validations + for problem in problem_list: + msg = f"Yamllint error: {file_path}:{problem.line}:{problem.column}: {problem.message} ({problem.rule})" + logger.error(msg) + # Note: NOT adding to self.errors to allow other validations to continue + + except ImportError: + logger.warning("yamllint not installed - skipping yamllint validation") + except Exception as e: + logger.warning(f"yamllint validation failed for {file_path}: {e}") + def _validate_syntax_file(self, file_path: Path, strict: bool = True) -> None: """Run syntactic validation for a single file""" if os.path.isfile(file_path) and file_path.suffix in [".yaml", ".yml"]: @@ -107,6 +145,10 @@ def _validate_syntax_file(self, file_path: Path, strict: bool = True) -> None: logger.error(msg) self.errors.append(msg) + # Run yamllint after other validations (if enabled) + if self.enable_yamllint: + self._run_yamllint(file_path) + def _get_named_path(self, data: dict[str, Any], path: str) -> str: """Convert a numeric path to a named path for better error messages.""" path_segments = path.split(".") diff --git a/pyproject.toml b/pyproject.toml index b12ac57..b6b0c86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "typer>=0.17.4", "yamale>=6.0.0", "jmespath>=1.0.0", + "yamllint>=1.38.0", ] [project.urls]