Merakit-Deploy/gitea/gitea_deployer/config.py

188 lines
5.9 KiB
Python

"""
Configuration module for deployment settings
Centralized configuration with validation from environment variables and CLI arguments
"""
import logging
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
class ConfigurationError(Exception):
"""Raised when configuration is invalid"""
pass
@dataclass
class DeploymentConfig:
"""Main deployment configuration loaded from environment and CLI args"""
# File paths (required - no defaults)
env_file: Path
docker_compose_file: Path
# Cloudflare credentials (required - no defaults)
cloudflare_api_token: str = field(repr=False) # Hide in logs
cloudflare_zone_id: str
# File paths (with defaults)
dict_file: Path = Path("/usr/share/dict/words")
# Domain settings
base_domain: str = "merakit.my"
app_name: Optional[str] = None
# Deployment options
dry_run: bool = False
max_retries: int = 3
healthcheck_timeout: int = 60 # seconds
healthcheck_interval: int = 10 # seconds
verify_ssl: bool = False
# Webhook settings (optional)
webhook_url: Optional[str] = None
webhook_timeout: int = 10 # seconds
webhook_retries: int = 3
# Logging
log_level: str = "INFO"
@classmethod
def from_env_and_args(cls, args) -> "DeploymentConfig":
"""
Factory method to create config from environment and CLI args
Args:
args: argparse.Namespace with CLI arguments
Returns:
DeploymentConfig instance
Raises:
ConfigurationError: If required configuration is missing
"""
logger.debug("Loading configuration from environment and arguments")
# Get Cloudflare credentials from environment
cloudflare_api_token = os.getenv('CLOUDFLARE_API_TOKEN')
cloudflare_zone_id = os.getenv('CLOUDFLARE_ZONE_ID')
if not cloudflare_api_token:
raise ConfigurationError(
"CLOUDFLARE_API_TOKEN environment variable is required"
)
if not cloudflare_zone_id:
raise ConfigurationError(
"CLOUDFLARE_ZONE_ID environment variable is required"
)
# Get optional webhook URL from environment or args
webhook_url = (
getattr(args, 'webhook_url', None)
or os.getenv('DEPLOYMENT_WEBHOOK_URL')
)
# Get optional settings from environment with defaults
max_retries = int(os.getenv('DEPLOYMENT_MAX_RETRIES', args.max_retries))
healthcheck_timeout = int(
os.getenv('DEPLOYMENT_HEALTHCHECK_TIMEOUT', '60')
)
healthcheck_interval = int(
os.getenv('DEPLOYMENT_HEALTHCHECK_INTERVAL', '10')
)
config = cls(
env_file=args.env_file,
docker_compose_file=args.compose_file,
dict_file=Path("/usr/share/dict/words"),
cloudflare_api_token=cloudflare_api_token,
cloudflare_zone_id=cloudflare_zone_id,
base_domain="merakit.my",
app_name=None,
dry_run=args.dry_run,
max_retries=max_retries,
healthcheck_timeout=healthcheck_timeout,
healthcheck_interval=healthcheck_interval,
verify_ssl=not args.no_verify_ssl,
webhook_url=webhook_url,
webhook_timeout=10,
webhook_retries=3,
log_level=args.log_level
)
logger.debug(f"Configuration loaded: {config}")
return config
def validate(self) -> None:
"""
Validate configuration completeness and correctness
Raises:
ConfigurationError: If configuration is invalid
"""
logger.debug("Validating configuration")
# Validate file paths exist
if not self.env_file.exists():
raise ConfigurationError(f"Env file not found: {self.env_file}")
if not self.docker_compose_file.exists():
raise ConfigurationError(
f"Docker compose file not found: {self.docker_compose_file}"
)
if not self.dict_file.exists():
raise ConfigurationError(
f"Dictionary file not found: {self.dict_file}. "
"Install 'words' package or ensure /usr/share/dict/words exists."
)
# Validate numeric ranges
if self.max_retries < 1:
raise ConfigurationError(
f"max_retries must be >= 1, got: {self.max_retries}"
)
if self.healthcheck_timeout < 1:
raise ConfigurationError(
f"healthcheck_timeout must be >= 1, got: {self.healthcheck_timeout}"
)
if self.healthcheck_interval < 1:
raise ConfigurationError(
f"healthcheck_interval must be >= 1, got: {self.healthcheck_interval}"
)
if self.healthcheck_interval >= self.healthcheck_timeout:
raise ConfigurationError(
f"healthcheck_interval ({self.healthcheck_interval}) must be < "
f"healthcheck_timeout ({self.healthcheck_timeout})"
)
# Validate log level
valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
if self.log_level.upper() not in valid_log_levels:
raise ConfigurationError(
f"Invalid log_level: {self.log_level}. "
f"Must be one of: {', '.join(valid_log_levels)}"
)
logger.debug("Configuration validation successful")
def __repr__(self) -> str:
"""String representation with masked sensitive values"""
return (
f"DeploymentConfig("
f"env_file={self.env_file}, "
f"dry_run={self.dry_run}, "
f"max_retries={self.max_retries}, "
f"cloudflare_api_token=*****, "
f"webhook_url={self.webhook_url})"
)