mirror of
https://github.com/philippemerle/KubeDiagrams.git
synced 2026-08-16 14:16:14 +00:00
236 lines
7.2 KiB
Python
236 lines
7.2 KiB
Python
"""Validation of user inputs."""
|
|
import re
|
|
import shlex
|
|
from typing import Optional, Tuple
|
|
from constants import MANIFEST_RE, KIND_RE, EXTRA_ARGS_ALLOWED_FLAGS
|
|
|
|
|
|
class ValidationError(Exception):
|
|
"""Exception error validation."""
|
|
pass
|
|
|
|
|
|
class InputValidator:
|
|
"""Validator for user inputs."""
|
|
|
|
SUPPORTED_FORMATS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'pdf', 'dot', 'dot_json', 'drawio', 'mermaid', 'd2']
|
|
|
|
HELM_SCHEME_URL_PATTERN = re.compile(r'^(https?|oci|file)://[a-zA-Z0-9\-_./:~]*$')
|
|
HELM_CHART_REF_PATTERN = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-_]*/[a-zA-Z0-9\-_]+$')
|
|
|
|
@classmethod
|
|
def is_valid_helm_url(cls, url: str) -> bool:
|
|
return bool(
|
|
cls.HELM_SCHEME_URL_PATTERN.fullmatch(url)
|
|
or cls.HELM_CHART_REF_PATTERN.fullmatch(url)
|
|
)
|
|
|
|
@classmethod
|
|
def validate_manifest(cls, content: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate a Kubernetes manifest content.
|
|
|
|
Args:
|
|
content: Manifest content
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
"""
|
|
if not content or not content.strip():
|
|
return False, "Manifest content cannot be empty."
|
|
|
|
# Verify minimum length
|
|
if len(content.strip()) < 10:
|
|
return False, "Manifest content is too short."
|
|
|
|
# Verify it looks like a manifest K8s
|
|
if not cls.looks_like_manifest(content):
|
|
return False, "Content does not appear to be a valid Kubernetes manifest (missing apiVersion or kind)."
|
|
|
|
return True, None
|
|
|
|
@classmethod
|
|
def validate_helmfile(cls, content: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate a Helmfile content.
|
|
|
|
Args:
|
|
content: Content of a Helmfile
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
"""
|
|
if not content or not content.strip():
|
|
return False, "Helmfile content cannot be empty."
|
|
|
|
if len(content.strip()) < 10:
|
|
return False, "Helmfile content is too short."
|
|
|
|
# Verify it looks like a Helmfile
|
|
if not cls.looks_like_helmfile(content):
|
|
return False, "Content does not appear to be a valid Helmfile (missing typical Helmfile keys)."
|
|
|
|
return True, None
|
|
|
|
@classmethod
|
|
def validate_helm_chart_url(cls, url: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate a Helm chart URL.
|
|
|
|
Args:
|
|
url: URL of the Helm chart
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
"""
|
|
if not url or not url.strip():
|
|
return False, "Chart URL cannot be empty."
|
|
|
|
url = url.strip()
|
|
|
|
# Verify the helm url pattern
|
|
if not cls.is_valid_helm_url(url):
|
|
return False, "Invalid Helm chart URL format. Must start with http://, https://, oci://, file://, or be a chart reference."
|
|
|
|
return True, None
|
|
|
|
@classmethod
|
|
def validate_output_format(cls, format_str: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate output format.
|
|
|
|
Args:
|
|
format_str: output format
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
"""
|
|
if not format_str:
|
|
return False, "Output format cannot be empty."
|
|
|
|
format_str = format_str.lower().strip()
|
|
|
|
if format_str not in cls.SUPPORTED_FORMATS:
|
|
return False, f"Unsupported output format '{format_str}'. Supported formats: {', '.join(cls.SUPPORTED_FORMATS)}"
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def find_disallowed_flag(tokens: list, tool: str) -> Optional[str]:
|
|
"""
|
|
Return the first token that looks like a CLI flag not in that tool's
|
|
allowlist (EXTRA_ARGS_ALLOWED_FLAGS), or None if all tokens are allowed.
|
|
|
|
Args:
|
|
tokens: Already-tokenized extra args (see shlex.split)
|
|
tool: Key into EXTRA_ARGS_ALLOWED_FLAGS identifying the target CLI tool
|
|
|
|
Returns:
|
|
Optional[str]: The disallowed flag, or None if all tokens are allowed
|
|
"""
|
|
allowed_flags = EXTRA_ARGS_ALLOWED_FLAGS[tool]
|
|
for token in tokens:
|
|
if token.startswith('-'):
|
|
flag = token.split('=', 1)[0]
|
|
if flag not in allowed_flags:
|
|
return flag
|
|
return None
|
|
|
|
@classmethod
|
|
def validate_extra_args(cls, args: str, tool: str) -> Tuple[bool, Optional[str]]:
|
|
"""
|
|
Validate extra args against the allowlist of flags for the given tool.
|
|
|
|
Args:
|
|
args: extra_args
|
|
tool: Key into EXTRA_ARGS_ALLOWED_FLAGS identifying the target CLI tool
|
|
|
|
Returns:
|
|
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
"""
|
|
if not args or not args.strip():
|
|
return True, None
|
|
|
|
try:
|
|
tokens = shlex.split(args.strip())
|
|
except ValueError:
|
|
return False, "Invalid extraArgs: could not parse the value (check for unmatched quotes)."
|
|
|
|
bad_flag = cls.find_disallowed_flag(tokens, tool)
|
|
if bad_flag:
|
|
allowed_flags = EXTRA_ARGS_ALLOWED_FLAGS[tool]
|
|
return False, (
|
|
f"Extra arg flag '{bad_flag}' is not allowed. "
|
|
f"Allowed flags: {', '.join(sorted(allowed_flags))}"
|
|
)
|
|
|
|
return True, None
|
|
|
|
@staticmethod
|
|
def validate_k8s_name(name: str) -> bool:
|
|
"""
|
|
Validate a Kubernetes resource name.
|
|
|
|
Args:
|
|
name: Resource name to validate
|
|
|
|
Returns:
|
|
bool: True if valid
|
|
"""
|
|
if not name or not isinstance(name, str):
|
|
return False
|
|
# Kubernetes names must be lowercase alphanumeric, with hyphens allowed
|
|
# Must start and end with alphanumeric
|
|
pattern = re.compile(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$')
|
|
return bool(pattern.match(name)) and len(name) <= 253
|
|
|
|
@staticmethod
|
|
def looks_like_manifest(text: str) -> bool:
|
|
"""
|
|
Heuristic to detect a Kubernetes manifest.
|
|
|
|
Args:
|
|
text: content that will be checked
|
|
|
|
Returns:
|
|
bool: True if it looks like a manifest
|
|
"""
|
|
if not text:
|
|
return False
|
|
t = text.strip()
|
|
return bool(MANIFEST_RE.search(t) and KIND_RE.search(t))
|
|
|
|
@staticmethod
|
|
def looks_like_helmfile(text: str) -> bool:
|
|
"""
|
|
Heuristic to detect a HelmFile.
|
|
|
|
Args:
|
|
text: Content that will be checked.
|
|
|
|
Returns:
|
|
bool: True if it looks like a Helmfile
|
|
"""
|
|
if not text:
|
|
return False
|
|
t = text.lower()
|
|
helmfile_keys = ["\nreleases:", "\nrepositories:", "\nhelmdefaults:", "\nenvironments:", "\ntemplates:"]
|
|
return any(key in t for key in helmfile_keys)
|
|
|
|
@classmethod
|
|
def sanitize_filename(cls, filename: str) -> str:
|
|
"""
|
|
Clean a filename to make it safe.
|
|
|
|
Args:
|
|
filename: Filename to clean
|
|
|
|
Returns:
|
|
str: Filename cleaned
|
|
"""
|
|
safe_filename = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)
|
|
# Length limit
|
|
if len(safe_filename) > 255:
|
|
safe_filename = safe_filename[:255]
|
|
return safe_filename
|