Update composer.py

This commit is contained in:
HHF Technology 2024-12-05 23:38:50 +05:30
parent 8d00e487cb
commit 686cb71933

View file

@ -1,8 +1,11 @@
#!/usr/bin/env python3
import os import os
from pathlib import Path from pathlib import Path
from typing import List, Dict from typing import List, Dict, Optional
import logging import logging
from docker_wrapper import Docker from docker_wrapper import Docker, NoContainersError
import sys
class ConfigError(Exception): class ConfigError(Exception):
@ -10,118 +13,200 @@ class ConfigError(Exception):
pass pass
class DockerComposer: class DockerComposeManager:
def __init__(self, config_file: str = 'docker-composer.conf'): def __init__(self, config_file: str = 'docker-composer.conf', debug: bool = False):
"""Initialize DockerComposer with configuration file.""" """
Initialize Docker Compose Manager.
Args:
config_file: Path to configuration file
debug: Enable debug logging
"""
self.debug = debug
self.logger = self._setup_logger() self.logger = self._setup_logger()
self.working_dir = Path.cwd() self.working_dir = Path.cwd()
self.config = self._load_config(config_file) self.config = self._load_config(config_file)
self.compose_dirs = self._get_compose_dirs()
self.containers = self._get_containers()
def _setup_logger(self) -> logging.Logger: def _setup_logger(self) -> logging.Logger:
"""Set up logging configuration.""" """Configure logging."""
logger = logging.getLogger('DockerComposer') logger = logging.getLogger('DockerComposeManager')
logger.setLevel(logging.INFO) logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
formatter = logging.Formatter('%(message)s')
handler = logging.StreamHandler() handler = logging.StreamHandler()
formatter = logging.Formatter('%(message)s')
handler.setFormatter(formatter) handler.setFormatter(formatter)
logger.addHandler(handler) logger.addHandler(handler)
return logger return logger
def _load_config(self, config_file: str) -> Dict[str, any]: def _load_config(self, config_file: str) -> Dict[str, any]:
"""Load and parse configuration file.""" """
Load configuration from file.
Args:
config_file: Path to configuration file
Returns:
Dictionary containing configuration
Raises:
ConfigError: If configuration file is invalid or missing
"""
try:
if not os.path.exists(config_file): if not os.path.exists(config_file):
raise ConfigError(f"Configuration file '{config_file}' not found") raise ConfigError(f"Configuration file '{config_file}' not found")
try:
with open(config_file, 'rt') as f: with open(config_file, 'rt') as f:
lines = f.readlines() lines = f.readlines()
if len(lines) < 2:
raise ConfigError("Incomplete configuration file")
config = {} config = {}
# Parse compose path # Parse compose path
compose_path = self._parse_config_line(lines[0], 'compose-path') compose_line = next((line for line in lines if 'compose-path' in line), None)
if not compose_line:
raise ConfigError("compose-path not found in configuration")
compose_path = compose_line.split('=')[1].strip()
compose_path = Path(compose_path if compose_path.startswith('/') compose_path = Path(compose_path if compose_path.startswith('/')
else self.working_dir / compose_path) else self.working_dir / compose_path)
config['compose_path'] = compose_path.resolve() config['compose_path'] = compose_path.resolve()
# Parse exclude containers # Parse exclude containers
exclude_str = self._parse_config_line(lines[1], 'exclude-containers') exclude_line = next((line for line in lines if 'exclude-containers' in line), None)
if not exclude_line:
raise ConfigError("exclude-containers not found in configuration")
exclude_str = exclude_line.split('=')[1].strip()
config['exclude_containers'] = [ config['exclude_containers'] = [
container.strip() container.strip()
for container in exclude_str.split(',') for container in exclude_str.split(',')
if container.strip() if container.strip()
] ]
if self.debug:
self.logger.debug(f"Working directory: {self.working_dir}")
self.logger.debug(f"Compose path: {config['compose_path']}")
self.logger.debug(f"Exclude containers: {config['exclude_containers']}")
return config return config
except IndexError:
raise ConfigError("Configuration file is incomplete")
except Exception as e: except Exception as e:
raise ConfigError(f"Failed to load configuration: {str(e)}") raise ConfigError(f"Failed to load configuration: {str(e)}")
def _parse_config_line(self, line: str, key: str) -> str: def _get_compose_dirs(self) -> List[Path]:
"""Parse a configuration line to extract its value.""" """Get list of valid compose directories."""
if '=' not in line: compose_dirs = []
raise ConfigError(f"Invalid configuration format for {key}")
return line.split('=', 1)[1].strip()
def _get_compose_directories(self) -> Dict[str, Path]: try:
"""Get valid Docker compose directories.""" if not self.config['compose_path'].exists():
compose_dirs = {} raise ConfigError(f"Compose path '{self.config['compose_path']}' does not exist")
compose_path = self.config['compose_path']
if not compose_path.exists(): for item in self.config['compose_path'].iterdir():
raise ConfigError(f"Compose path '{compose_path}' does not exist") if (item.is_dir() and
not item.name.startswith('.') and
for item in compose_path.iterdir(): item.name not in self.config['exclude_containers']):
# Skip if not a directory or hidden # Verify docker-compose.yml exists
if not item.is_dir() or item.name.startswith('.'): if (item / 'docker-compose.yml').exists():
continue compose_dirs.append(item)
else:
# Skip excluded containers
if item.name in self.config['exclude_containers']:
self.logger.info(f"Skipping excluded container: {item.name}")
continue
# Check for docker-compose.yml
if not (item / 'docker-compose.yml').exists():
self.logger.warning(f"Skipping {item.name}: no docker-compose.yml found") self.logger.warning(f"Skipping {item.name}: no docker-compose.yml found")
continue
compose_dirs[item.name] = item if self.debug:
self.logger.debug(f"Compose directories: {compose_dirs}")
return compose_dirs return sorted(compose_dirs)
def compose(self) -> None: except Exception as e:
"""Execute Docker compose for all valid directories.""" raise ConfigError(f"Failed to get compose directories: {str(e)}")
compose_dirs = self._get_compose_directories()
if not compose_dirs: def _get_containers(self) -> List[str]:
"""Get list of container names from compose directories."""
return [dir.name for dir in self.compose_dirs]
def recompose_all(self) -> None:
"""Recompose all containers."""
for i, container in enumerate(self.containers):
try:
self.logger.info(f"\nRecomposing {container}...")
if self.recompose_container(i):
self.logger.info(f"{container} recomposed successfully!")
else:
self.logger.error(f"Failed to recompose {container}")
except Exception as e:
self.logger.error(f"Error recomposing {container}: {str(e)}")
def recompose_container(self, container_index: int) -> bool:
"""
Recompose a specific container.
Args:
container_index: Index of container to recompose
Returns:
bool: True if successful, False otherwise
"""
try:
container = self.containers[container_index]
container_dir = self.compose_dirs[container_index]
# Stop and remove existing container if it exists
if Docker.containers_exist():
try:
self.logger.info("Stopping container...")
Docker.stop(container)
self.logger.info("Removing container...")
Docker.rm(container)
except Exception as e:
self.logger.warning(f"Container cleanup failed: {e}")
# Compose new container
self.logger.info("Composing container...")
status, output = Docker.compose(str(container_dir))
if status == 0:
return True
else:
self.logger.error(f"Compose failed: {output}")
return False
except Exception as e:
self.logger.error(f"Error recomposing container: {str(e)}")
return False
def run(self) -> None:
"""Main execution method - recompose all containers."""
try:
if not self.compose_dirs:
self.logger.warning("No valid compose directories found") self.logger.warning("No valid compose directories found")
return return
for container_name, directory in compose_dirs.items(): self.logger.info(f"Found {len(self.containers)} containers to compose")
try: self.recompose_all()
self.logger.info(f"COMPOSING {container_name}...")
Docker.compose(str(directory)) except KeyboardInterrupt:
self.logger.info("DONE!") self.logger.info("\nOperation cancelled by user")
sys.exit(0)
except Exception as e: except Exception as e:
self.logger.error(f"Failed to compose {container_name}: {str(e)}") self.logger.error(f"Unexpected error: {str(e)}")
sys.exit(1)
def main(): def main():
"""Main entry point for the Docker composer.""" """Main entry point."""
try: try:
composer = DockerComposer() manager = DockerComposeManager(debug=False)
composer.compose() manager.run()
except ConfigError as e: except ConfigError as e:
print(f"Configuration error: {str(e)}") print(f"Configuration error: {str(e)}", file=sys.stderr)
exit(1) sys.exit(1)
except Exception as e: except Exception as e:
print(f"Unexpected error: {str(e)}") print(f"Unexpected error: {str(e)}", file=sys.stderr)
exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":