188 lines
5.9 KiB
Python
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})"
|
|
)
|