135 lines
3.9 KiB
Python
Executable file
135 lines
3.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Script Name: check-helm-chart-versions.py
|
|
Description:
|
|
v1.2.0 - 2026-06-09: Added usage example in docstring and --help.
|
|
v1.1.0 - 2026-06-09: Generic Helm chart version drift checker.
|
|
v1.0.0 - 2026-06-08: Initial release.
|
|
|
|
Example layout::
|
|
|
|
charts/
|
|
monitoring/
|
|
alpha/Chart.yaml # dependencies version: 1.2.0
|
|
beta/Chart.yaml # dependencies version: 1.3.0
|
|
|
|
Run::
|
|
|
|
./check-helm-chart-versions.py charts
|
|
|
|
Example output::
|
|
|
|
=== MISALIGNMENT FOUND: monitoring ===
|
|
alpha 1.2.0
|
|
beta 1.3.0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
_DEPENDENCY_RE = re.compile(
|
|
r"dependencies:\s*\n(?:[^\n]*\n)*?\s*-\s*name:[^\n]*\n(?:[^\n]*\n)*?\s*version:\s*([^\n]+)",
|
|
)
|
|
_APP_VERSION_RE = re.compile(r"appVersion:\s*([^\n]+)")
|
|
|
|
|
|
def extract_version(chart_file: Path) -> str | None:
|
|
content = chart_file.read_text(encoding="utf-8")
|
|
match = _DEPENDENCY_RE.search(content)
|
|
if match:
|
|
return match.group(1).strip().strip("\"'")
|
|
match = _APP_VERSION_RE.search(content)
|
|
if match:
|
|
return match.group(1).strip().strip("\"'")
|
|
return None
|
|
|
|
|
|
def parse_chart_location(chart_file: Path, root: Path) -> tuple[str, str] | None:
|
|
"""
|
|
Map a Chart.yaml path to (group, target).
|
|
|
|
group: parent directories relative to root, excluding the leaf directory
|
|
target: leaf directory name immediately containing Chart.yaml
|
|
"""
|
|
try:
|
|
rel_parent = chart_file.parent.relative_to(root)
|
|
except ValueError:
|
|
return None
|
|
if not rel_parent.parts:
|
|
return None
|
|
target = rel_parent.parts[-1]
|
|
group = "/".join(rel_parent.parts[:-1]) if len(rel_parent.parts) > 1 else "."
|
|
return group, target
|
|
|
|
|
|
def find_chart_files(root: Path) -> list[Path]:
|
|
return sorted(root.rglob("Chart.yaml"))
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Find Helm chart version drift across directory variants.",
|
|
epilog=(
|
|
"example:\n"
|
|
" %(prog)s charts\n"
|
|
" %(prog)s charts --min-depth 3\n\n"
|
|
"Given charts/monitoring/alpha/Chart.yaml (1.2.0) and\n"
|
|
"charts/monitoring/beta/Chart.yaml (1.3.0), reports a mismatch\n"
|
|
"under group 'monitoring'."
|
|
),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"root",
|
|
nargs="?",
|
|
default=".",
|
|
type=Path,
|
|
help="Directory tree to scan (default: current directory)",
|
|
)
|
|
parser.add_argument(
|
|
"--min-depth",
|
|
type=int,
|
|
default=2,
|
|
metavar="N",
|
|
help="Minimum path depth below root for Chart.yaml (default: 2)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
root = args.root.resolve()
|
|
if not root.is_dir():
|
|
print(f"Error: not a directory: {root}", file=sys.stderr)
|
|
return 1
|
|
|
|
app_data: dict[str, dict[str, str]] = defaultdict(dict)
|
|
for chart_file in find_chart_files(root):
|
|
if len(chart_file.parent.relative_to(root).parts) < args.min_depth:
|
|
continue
|
|
location = parse_chart_location(chart_file, root)
|
|
version = extract_version(chart_file)
|
|
if not location or not version:
|
|
continue
|
|
group, target = location
|
|
app_data[group][target] = version
|
|
|
|
misalignments_found = False
|
|
for group in sorted(app_data):
|
|
targets = app_data[group]
|
|
if len(set(targets.values())) > 1:
|
|
misalignments_found = True
|
|
print(f"=== MISALIGNMENT FOUND: {group} ===")
|
|
for target in sorted(targets):
|
|
print(f" {target:20s} {targets[target]}")
|
|
print()
|
|
|
|
if not misalignments_found:
|
|
print("No version misalignments found.")
|
|
return 1 if misalignments_found else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|