Update composer.py
This commit is contained in:
parent
8d00e487cb
commit
686cb71933
1 changed files with 152 additions and 67 deletions
219
composer.py
219
composer.py
|
@ -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."""
|
"""
|
||||||
if not os.path.exists(config_file):
|
Load configuration from file.
|
||||||
raise ConfigError(f"Configuration file '{config_file}' not found")
|
|
||||||
|
Args:
|
||||||
|
config_file: Path to configuration file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing configuration
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ConfigError: If configuration file is invalid or missing
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
raise ConfigError(f"Configuration file '{config_file}' not found")
|
||||||
|
|
||||||
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()
|
try:
|
||||||
|
if not self.config['compose_path'].exists():
|
||||||
|
raise ConfigError(f"Compose path '{self.config['compose_path']}' does not exist")
|
||||||
|
|
||||||
|
for item in self.config['compose_path'].iterdir():
|
||||||
|
if (item.is_dir() and
|
||||||
|
not item.name.startswith('.') and
|
||||||
|
item.name not in self.config['exclude_containers']):
|
||||||
|
# Verify docker-compose.yml exists
|
||||||
|
if (item / 'docker-compose.yml').exists():
|
||||||
|
compose_dirs.append(item)
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"Skipping {item.name}: no docker-compose.yml found")
|
||||||
|
|
||||||
def _get_compose_directories(self) -> Dict[str, Path]:
|
if self.debug:
|
||||||
"""Get valid Docker compose directories."""
|
self.logger.debug(f"Compose directories: {compose_dirs}")
|
||||||
compose_dirs = {}
|
|
||||||
compose_path = self.config['compose_path']
|
|
||||||
|
|
||||||
if not compose_path.exists():
|
return sorted(compose_dirs)
|
||||||
raise ConfigError(f"Compose path '{compose_path}' does not exist")
|
|
||||||
|
|
||||||
for item in compose_path.iterdir():
|
except Exception as e:
|
||||||
# Skip if not a directory or hidden
|
raise ConfigError(f"Failed to get compose directories: {str(e)}")
|
||||||
if not item.is_dir() or item.name.startswith('.'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip excluded containers
|
def _get_containers(self) -> List[str]:
|
||||||
if item.name in self.config['exclude_containers']:
|
"""Get list of container names from compose directories."""
|
||||||
self.logger.info(f"Skipping excluded container: {item.name}")
|
return [dir.name for dir in self.compose_dirs]
|
||||||
continue
|
|
||||||
|
|
||||||
# Check for docker-compose.yml
|
def recompose_all(self) -> None:
|
||||||
if not (item / 'docker-compose.yml').exists():
|
"""Recompose all containers."""
|
||||||
self.logger.warning(f"Skipping {item.name}: no docker-compose.yml found")
|
for i, container in enumerate(self.containers):
|
||||||
continue
|
|
||||||
|
|
||||||
compose_dirs[item.name] = item
|
|
||||||
|
|
||||||
return compose_dirs
|
|
||||||
|
|
||||||
def compose(self) -> None:
|
|
||||||
"""Execute Docker compose for all valid directories."""
|
|
||||||
compose_dirs = self._get_compose_directories()
|
|
||||||
|
|
||||||
if not compose_dirs:
|
|
||||||
self.logger.warning("No valid compose directories found")
|
|
||||||
return
|
|
||||||
|
|
||||||
for container_name, directory in compose_dirs.items():
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"COMPOSING {container_name}...")
|
self.logger.info(f"\nRecomposing {container}...")
|
||||||
Docker.compose(str(directory))
|
if self.recompose_container(i):
|
||||||
self.logger.info("DONE!")
|
self.logger.info(f"{container} recomposed successfully!")
|
||||||
|
else:
|
||||||
|
self.logger.error(f"Failed to recompose {container}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Failed to compose {container_name}: {str(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")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.info(f"Found {len(self.containers)} containers to compose")
|
||||||
|
self.recompose_all()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.logger.info("\nOperation cancelled by user")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as 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__":
|
||||||
|
|
Loading…
Reference in a new issue