Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions docker/Taskfile.docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion docker/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
55 changes: 30 additions & 25 deletions scripts/labctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def create_network_if_missing(network_name: str) -> None:
Expand Down Expand Up @@ -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

Expand All @@ -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)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
logger.info(f">>> Starting {stack_dir}/{service_name}")
docker(["compose", "-f", compose_file, *env_file_args, "up", "--detach"])

Expand All @@ -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"])

Expand All @@ -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.")
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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')
Comment thread
bubacoder marked this conversation as resolved.

# 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()

Expand Down
6 changes: 3 additions & 3 deletions scripts/task-mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"
Expand Down