Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion nac_validate/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down
44 changes: 43 additions & 1 deletion nac_validate/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -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(".")
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"typer>=0.17.4",
"yamale>=6.0.0",
"jmespath>=1.0.0",
"yamllint>=1.38.0",
]

[project.urls]
Expand Down
Loading