Source code for kstlib.pipeline.validators
"""Input validation for kstlib.pipeline module.
This module provides validation functions for pipeline configuration,
implementing deep defense against malformed or malicious input.
Reuses security validators from ``kstlib.ops.validators`` for command
and environment variable validation.
"""
from __future__ import annotations
import re
from kstlib.ops.validators import (
DANGEROUS_PATTERNS,
validate_command,
validate_env,
)
from kstlib.pipeline.exceptions import PipelineConfigError
from kstlib.utils.validators import (
CALLABLE_TARGET_PATTERN,
MAX_CALLABLE_TARGET_LENGTH,
_validate_callable_target_str,
)
# ============================================================================
# Constants - Hard Limits
# ============================================================================
#: Maximum step name length.
MAX_STEP_NAME_LENGTH = 64
#: Pattern for valid step names (same rules as session names).
STEP_NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
#: Maximum number of steps in a single pipeline.
MAX_PIPELINE_STEPS = 50
#: Maximum number of arguments for a step.
MAX_STEP_ARGS = 50
#: Pattern for valid Python module names.
MODULE_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_.]*$")
#: Maximum length of a module name.
MAX_MODULE_NAME_LENGTH = 256
# ============================================================================
# Validation Functions
# ============================================================================
[docs]
def validate_step_name(name: str) -> str:
"""Validate and return a step name.
Rules:
- Cannot be empty
- Max 64 characters (hard limit)
- Must start with a letter
- Only alphanumeric, underscore, hyphen allowed
Args:
name: Step name to validate.
Returns:
The validated step name (unchanged).
Raises:
PipelineConfigError: If name is invalid.
Examples:
>>> validate_step_name("build_logs")
'build_logs'
>>> validate_step_name("step-01")
'step-01'
>>> validate_step_name("")
Traceback (most recent call last):
...
kstlib.pipeline.exceptions.PipelineConfigError: Step name cannot be empty
"""
if not name:
raise PipelineConfigError("Step name cannot be empty")
if len(name) > MAX_STEP_NAME_LENGTH:
raise PipelineConfigError(f"Step name too long (max {MAX_STEP_NAME_LENGTH} chars)")
if not STEP_NAME_PATTERN.match(name):
raise PipelineConfigError(
"Step name must start with a letter and contain only alphanumeric, underscore, or hyphen characters"
)
return name
[docs]
def validate_callable_target(target: str) -> str:
"""Validate a callable target string.
Expected format: ``module.path:function_name``
Args:
target: Callable target string to validate.
Returns:
The validated target (unchanged).
Raises:
PipelineConfigError: If target is invalid.
Examples:
>>> validate_callable_target("mymodule:run")
'mymodule:run'
>>> validate_callable_target("my.pkg.module:do_work")
'my.pkg.module:do_work'
"""
try:
_validate_callable_target_str(target)
except ValueError as exc:
raise PipelineConfigError(str(exc)) from exc
return target
[docs]
def validate_module_name(module: str) -> str:
"""Validate a Python module name.
Args:
module: Module name to validate (e.g. ``my.package.module``).
Returns:
The validated module name (unchanged).
Raises:
PipelineConfigError: If module name is invalid.
Examples:
>>> validate_module_name("mymodule")
'mymodule'
>>> validate_module_name("my.package.module")
'my.package.module'
"""
if not module:
raise PipelineConfigError("Module name cannot be empty")
if len(module) > MAX_MODULE_NAME_LENGTH:
raise PipelineConfigError(f"Module name too long (max {MAX_MODULE_NAME_LENGTH} chars)")
if not MODULE_NAME_PATTERN.match(module):
raise PipelineConfigError(f"Invalid module name format: {module!r}")
return module
[docs]
def validate_step_config(
*,
name: str,
step_type: str,
command: str | None = None,
module: str | None = None,
callable_target: str | None = None,
args: list[str] | None = None,
env: dict[str, str] | None = None,
) -> None:
"""Validate a step configuration.
Checks that the step has the required fields for its type and
that all values pass validation.
Args:
name: Step name.
step_type: Step type (shell, python, callable).
command: Shell command (required for shell type).
module: Python module (required for python type).
callable_target: Import target (required for callable type).
args: Arguments list.
env: Environment variables.
Raises:
PipelineConfigError: If configuration is invalid.
"""
validate_step_name(name)
if step_type == "shell":
if not command:
raise PipelineConfigError(f"Step '{name}': shell step requires a 'command'")
validate_command(command, strict=False)
elif step_type == "python":
if not module:
raise PipelineConfigError(f"Step '{name}': python step requires a 'module'")
validate_module_name(module)
elif step_type == "callable":
if not callable_target:
raise PipelineConfigError(f"Step '{name}': callable step requires a 'callable' target")
validate_callable_target(callable_target)
else:
raise PipelineConfigError(
f"Step '{name}': unknown step type {step_type!r} (expected 'shell', 'python', or 'callable')"
)
if args is not None and len(args) > MAX_STEP_ARGS:
raise PipelineConfigError(f"Step '{name}': too many arguments (max {MAX_STEP_ARGS})")
if env is not None:
validate_env(env)
[docs]
def validate_pipeline_config(
*,
step_count: int,
on_error: str,
) -> None:
"""Validate pipeline-level configuration.
Args:
step_count: Number of steps in the pipeline.
on_error: Error policy string.
Raises:
PipelineConfigError: If configuration is invalid.
"""
if step_count == 0:
raise PipelineConfigError("Pipeline must have at least one step")
if step_count > MAX_PIPELINE_STEPS:
raise PipelineConfigError(f"Too many steps (max {MAX_PIPELINE_STEPS})")
if on_error not in ("fail_fast", "continue"):
raise PipelineConfigError(f"Invalid error policy {on_error!r} (expected 'fail_fast' or 'continue')")
__all__ = [
"CALLABLE_TARGET_PATTERN",
"DANGEROUS_PATTERNS",
"MAX_CALLABLE_TARGET_LENGTH",
"MAX_MODULE_NAME_LENGTH",
"MAX_PIPELINE_STEPS",
"MAX_STEP_ARGS",
"MAX_STEP_NAME_LENGTH",
"MODULE_NAME_PATTERN",
"STEP_NAME_PATTERN",
"validate_callable_target",
"validate_command",
"validate_env",
"validate_module_name",
"validate_pipeline_config",
"validate_step_config",
"validate_step_name",
]