diff --git a/CLAUDE.md b/CLAUDE.md index 49a10c2d..0105e0ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,13 +54,13 @@ task docker:create-example-env ### Service Management ```bash -# Manage individual Docker service (up, down, restart, recreate, update, pull, config) +# Manage individual Docker service (pull, up, down, restart, recreate, config) scripts/labctl.py service [operation] [category/service-name] # Examples: scripts/labctl.py service up security/traefik scripts/labctl.py service restart ai/ollama -scripts/labctl.py service update media/video/jellyfin +scripts/labctl.py service pull media/video/jellyfin ``` ### Ansible Commands diff --git a/docker/Taskfile.docker.yaml b/docker/Taskfile.docker.yaml index 5ffe94df..e8529389 100644 --- a/docker/Taskfile.docker.yaml +++ b/docker/Taskfile.docker.yaml @@ -11,21 +11,21 @@ tasks: cmds: - scripts/labctl.py config apply - update: - desc: Update and restart containers + pull-all: + desc: Pull all (even not enabled) container images cmds: - - scripts/labctl.py config apply --mode=update + - scripts/labctl.py config apply --mode=pull - pull: - desc: Pull container images + update: + desc: Update containers (pull before start) cmds: - - scripts/labctl.py config apply --mode=pull + - scripts/labctl.py config apply --pull-before-start - pull-update: - desc: Pull, update and restart containers, then show restarts + apply-update: + desc: Update containers (pull before start), then prune old images and show restart events cmds: - - task: pull - task: update + - task: prune - task: show-restarts stop: @@ -39,7 +39,7 @@ tasks: - docker stop $(docker ps -a -q) prune: - desc: Remove unused and dangling container images + desc: Remove unused and dangling container images created before given timestamp (21 days) cmds: - docker image prune --all --force --filter "until=504h" diff --git a/docker/guidelines.md b/docker/guidelines.md index c05ba564..7fc0f74d 100644 --- a/docker/guidelines.md +++ b/docker/guidelines.md @@ -133,7 +133,7 @@ Services are deployed using the Taskfile system: ``` task docker:apply # Deploy all containers task docker:update # Update and restart containers -task docker:pull # Pull latest container images +task docker:pull-all # Pull latest container images task docker:stop # Stop configured containers ``` diff --git a/scripts/labctl.py b/scripts/labctl.py index 80c3ed02..5f8d1bb8 100755 --- a/scripts/labctl.py +++ b/scripts/labctl.py @@ -20,7 +20,7 @@ # Global variables docker_stacks_dir: Path = (Path(__file__).resolve().parent.parent / "docker").resolve() -ALLOWED_STATES: tuple[str, ...] = ('up', 'update', 'pull', 'down', 'restart', 'recreate', 'config') +ALLOWED_STATES: tuple[str, ...] = ('pull', 'up', 'down', 'restart', 'recreate', 'config') def create_network_if_missing(network_name: str) -> None: @@ -90,7 +90,19 @@ def docker(cmd: list[str], env=None, stdin=None, stdout=None, stderr=None) -> No subprocess.run(["docker"] + cmd, env=env, stdin=stdin, stdout=stdout, stderr=stderr, check=True) -def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, action: str) -> None: +def docker_pull(stack_dir: Path, service_name: str, compose_file: Path, env_file_args: list[str]) -> None: + """Pull Docker images for a service.""" + logger.info(f">>> Pulling {stack_dir}/{service_name}") + if has_build_directive(compose_file): + # Bake: https://docs.docker.com/guides/compose-bake/ + env = os.environ.copy() + env["COMPOSE_BAKE"] = "true" + docker(["compose", "-f", compose_file, *env_file_args, "build", "--pull"], env=env) + else: + docker(["compose", "-f", compose_file, *env_file_args, "pull"]) + + +def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, action: str, pull_before_start: bool = False) -> None: """Execute Docker Compose command for a service.""" logger.info("") # separation @@ -101,21 +113,15 @@ def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, ac env_file_args = get_env_file_args(host_config_dir, service_name) - # Handle pull operations - if action in ["update", "pull"]: - logger.info(f">>> Pulling {stack_dir}/{service_name}") - - if has_build_directive(compose_file): - # Bake: https://docs.docker.com/guides/compose-bake/ - env = os.environ.copy() - env["COMPOSE_BAKE"] = "true" - docker(["compose", "-f", compose_file, *env_file_args, "build", "--pull"], env=env) - else: - docker(["compose", "-f", compose_file, *env_file_args, "pull"]) - # Handle other operations match action: - case "up" | "update": + case "pull": + docker_pull(stack_dir, service_name, compose_file, env_file_args) + + case "up": + if pull_before_start: + docker_pull(stack_dir, service_name, compose_file, env_file_args) + logger.info(f">>> Starting {stack_dir}/{service_name}") docker(["compose", "-f", compose_file, *env_file_args, "up", "--detach"]) @@ -128,6 +134,9 @@ def docker_command(host_config_dir: Path, stack_dir: Path, service_name: str, ac docker(["compose", "-f", compose_file, *env_file_args, "restart"]) case "recreate": + if pull_before_start: + docker_pull(stack_dir, service_name, compose_file, env_file_args) + logger.info(f">>> Recreating {stack_dir}/{service_name}") docker(["compose", "-f", compose_file, *env_file_args, "up", "--detach", "--force-recreate"]) @@ -147,7 +156,7 @@ def load_services_config(config_file: str) -> dict: sys.exit(1) -def process_services(host_config_dir: Path, config: dict, state_override: str | None = None) -> None: +def process_services(host_config_dir: Path, config: dict, state_override: str | None = None, pull_before_start: bool = False) -> None: """Process services based on the configuration.""" if not config or 'services' not in config: logger.error("Error: Invalid configuration format. 'services' key not found.") @@ -177,7 +186,7 @@ def process_services(host_config_dir: Path, config: dict, state_override: str | logger.warning(f"Unknown state '{state}' for service {category}/{name}") continue - docker_command(host_config_dir, docker_stacks_dir / category, name, state) + docker_command(host_config_dir, docker_stacks_dir / category, name, state, pull_before_start) def get_host_config_dir() -> Path: @@ -203,13 +212,7 @@ def cmd_config_apply(args) -> None: create_network_if_missing("proxy") # Process services with optional mode override - process_services(host_config_dir, config, args.mode) - - # Cleanup old images if enabled in config and in update mode - if args.mode == 'update': - logger.info("Cleanup...") - # Remove unused and dangling images created before given timestamp (21 days) - docker(["image", "prune", "--all", "--force", "--filter", "until=504h"]) + process_services(host_config_dir, config, args.mode, args.pull_before_start) def cmd_service(args) -> None: @@ -229,7 +232,7 @@ def cmd_service(args) -> None: # Everything before the last part is the category path category_path = '/'.join(name_parts[:-1]) - docker_command(get_host_config_dir(), docker_stacks_dir / category_path, service_name, args.operation) + docker_command(get_host_config_dir(), docker_stacks_dir / category_path, service_name, args.operation, args.pull_before_start) def main() -> None: @@ -244,11 +247,13 @@ def main() -> None: config_apply_parser = config_subparsers.add_parser('apply', help='Apply service configurations') config_apply_parser.add_argument('--config', '-c', help='Path to the YAML configuration file') config_apply_parser.add_argument('--mode', '-m', choices=list(ALLOWED_STATES), help='Override state for all services') + config_apply_parser.add_argument('--pull-before-start', action='store_true', default=False, help='Pull images before starting services') # Service command service_parser = subparsers.add_parser('service', help='Manage individual services') service_parser.add_argument('operation', choices=list(ALLOWED_STATES), help='Operation to perform on the service') service_parser.add_argument('name', help='Service name in format category/name or category/subcategory/name') + service_parser.add_argument('--pull-before-start', action='store_true', default=False, help='Pull images before starting the service') args = parser.parse_args() diff --git a/scripts/task-mcp/server.py b/scripts/task-mcp/server.py index c6bdd53c..0c616933 100755 --- a/scripts/task-mcp/server.py +++ b/scripts/task-mcp/server.py @@ -123,16 +123,16 @@ def task_fn() -> str: @mcp.tool(name="control-container-service") def control_container_service(operation: str, service_name: str) -> str: """ - Execute one operation ('up', 'down', 'restart', 'recreate', 'update', 'pull' or 'config') on the specified service and return the output + Execute one operation ('pull', 'up', 'down', 'restart', 'recreate', 'config') on the specified service and return the output Args: - operation: One of 'up', 'down', 'restart', 'recreate', 'update', 'pull', 'config' + operation: One of 'pull', 'up', 'down', 'restart', 'recreate', 'config' service_name: Service name in format category/name or category/subcategory/name Returns: The command output as a string """ - allowed_operations = ['up', 'down', 'restart', 'recreate', 'update', 'pull', 'config'] + allowed_operations = ['pull', 'up', 'down', 'restart', 'recreate', 'config'] if operation not in allowed_operations: return f"Invalid operation: {operation}. Allowed: {', '.join(allowed_operations)}"