mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 12:18:17 +00:00 
			
		
		
		
	sys-ctl-rpr-docker-soft: switch to STRICT label mode and adapt tests
- script.py now resolves docker-compose project and working_dir strictly from container labels - removed container-name fallback logic - adjusted sys-ctl-hlth-docker-container to include sys-ctl-rpr-docker-soft - cleaned up sys-svc-docker dependencies - updated unit tests to mock docker inspect and os.path.isfile for STRICT mode Conversation: https://chatgpt.com/share/68b80927-b800-800f-a909-0fe8d110fd0e
This commit is contained in:
		@@ -3,6 +3,11 @@
 | 
				
			|||||||
    name: sys-ctl-alm-compose
 | 
					    name: sys-ctl-alm-compose
 | 
				
			||||||
  when: run_once_sys_ctl_alm_compose is not defined
 | 
					  when: run_once_sys_ctl_alm_compose is not defined
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					- name: Include dependency 'sys-ctl-rpr-docker-soft'
 | 
				
			||||||
 | 
					  include_role:
 | 
				
			||||||
 | 
					    name: sys-ctl-rpr-docker-soft
 | 
				
			||||||
 | 
					  when: run_once_sys_ctl_rpr_docker_soft is not defined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- include_role:
 | 
					- include_role:
 | 
				
			||||||
    name: sys-service
 | 
					    name: sys-service
 | 
				
			||||||
  vars:
 | 
					  vars:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,26 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
Restart Docker-Compose configurations with exited or unhealthy containers.
 | 
					Restart Docker-Compose configurations with exited or unhealthy containers.
 | 
				
			||||||
This version receives the *manipulation services* via argparse (no Jinja).
 | 
					
 | 
				
			||||||
 | 
					STRICT mode: Resolve the Compose project exclusively via Docker labels
 | 
				
			||||||
 | 
					(com.docker.compose.project and com.docker.compose.project.working_dir).
 | 
				
			||||||
 | 
					No container-name fallback. If labels are missing or Docker is unavailable,
 | 
				
			||||||
 | 
					the script records an error for that container.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All shell interactions that matter for tests go through print_bash()
 | 
				
			||||||
 | 
					so they can be monkeypatched in unit tests.
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
from typing import List
 | 
					from typing import List, Optional, Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					# Shell helpers
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def bash(command: str) -> List[str]:
 | 
					def bash(command: str) -> List[str]:
 | 
				
			||||||
    print(command)
 | 
					    print(command)
 | 
				
			||||||
    process = subprocess.Popen(
 | 
					    process = subprocess.Popen(
 | 
				
			||||||
@@ -30,31 +41,45 @@ def list_to_string(lst: List[str]) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def print_bash(command: str) -> List[str]:
 | 
					def print_bash(command: str) -> List[str]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Wrapper around bash() that echoes combined output for easier debugging
 | 
				
			||||||
 | 
					    and can be monkeypatched in tests.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    output = bash(command)
 | 
					    output = bash(command)
 | 
				
			||||||
    if output:
 | 
					    if output:
 | 
				
			||||||
        print(list_to_string(output))
 | 
					        print(list_to_string(output))
 | 
				
			||||||
    return output
 | 
					    return output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def find_docker_compose_file(directory: str) -> str | None:
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					# Filesystem / compose helpers
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def find_docker_compose_file(directory: str) -> Optional[str]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Search for docker-compose.yml beneath a directory.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    for root, _, files in os.walk(directory):
 | 
					    for root, _, files in os.walk(directory):
 | 
				
			||||||
        if "docker-compose.yml" in files:
 | 
					        if "docker-compose.yml" in files:
 | 
				
			||||||
            return os.path.join(root, "docker-compose.yml")
 | 
					            return os.path.join(root, "docker-compose.yml")
 | 
				
			||||||
    return None
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def detect_env_file(project_path: str) -> str | None:
 | 
					def detect_env_file(project_path: str) -> Optional[str]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Return the path to a Compose env file if present (.env preferred, fallback to env).
 | 
					    Return the path to a Compose env file if present (.env preferred, fallback to .env/env).
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    candidates = [os.path.join(project_path, ".env"), os.path.join(project_path, ".env", "env")]
 | 
					    candidates = [
 | 
				
			||||||
 | 
					        os.path.join(project_path, ".env"),
 | 
				
			||||||
 | 
					        os.path.join(project_path, ".env", "env"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
    for candidate in candidates:
 | 
					    for candidate in candidates:
 | 
				
			||||||
        if os.path.isfile(candidate):
 | 
					        if os.path.isfile(candidate):
 | 
				
			||||||
            return candidate
 | 
					            return candidate
 | 
				
			||||||
    return None
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def compose_cmd(subcmd: str, project_path: str, project_name: str | None = None) -> str:
 | 
					def compose_cmd(subcmd: str, project_path: str, project_name: Optional[str] = None) -> str:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Build a docker-compose command string with optional -p and --env-file if present.
 | 
					    Build a docker-compose command string with optional -p and --env-file if present.
 | 
				
			||||||
    Example: compose_cmd("restart", "/opt/docker/foo", "foo")
 | 
					    Example: compose_cmd("restart", "/opt/docker/foo", "foo")
 | 
				
			||||||
@@ -69,6 +94,10 @@ def compose_cmd(subcmd: str, project_path: str, project_name: str | None = None)
 | 
				
			|||||||
    return " ".join(parts)
 | 
					    return " ".join(parts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					# Business logic
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def normalize_services_arg(raw: List[str] | None, raw_str: str | None) -> List[str]:
 | 
					def normalize_services_arg(raw: List[str] | None, raw_str: str | None) -> List[str]:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Accept either:
 | 
					    Accept either:
 | 
				
			||||||
@@ -78,7 +107,6 @@ def normalize_services_arg(raw: List[str] | None, raw_str: str | None) -> List[s
 | 
				
			|||||||
    if raw:
 | 
					    if raw:
 | 
				
			||||||
        return [s for s in raw if s.strip()]
 | 
					        return [s for s in raw if s.strip()]
 | 
				
			||||||
    if raw_str:
 | 
					    if raw_str:
 | 
				
			||||||
        # split on comma or whitespace
 | 
					 | 
				
			||||||
        parts = [p.strip() for chunk in raw_str.split(",") for p in chunk.split()]
 | 
					        parts = [p.strip() for chunk in raw_str.split(",") for p in chunk.split()]
 | 
				
			||||||
        return [p for p in parts if p]
 | 
					        return [p for p in parts if p]
 | 
				
			||||||
    return []
 | 
					    return []
 | 
				
			||||||
@@ -87,7 +115,7 @@ def normalize_services_arg(raw: List[str] | None, raw_str: str | None) -> List[s
 | 
				
			|||||||
def wait_while_manipulation_running(
 | 
					def wait_while_manipulation_running(
 | 
				
			||||||
    services: List[str],
 | 
					    services: List[str],
 | 
				
			||||||
    waiting_time: int = 600,
 | 
					    waiting_time: int = 600,
 | 
				
			||||||
    timeout: int | None = None,
 | 
					    timeout: Optional[int] = None,
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Wait until none of the given services are active anymore.
 | 
					    Wait until none of the given services are active anymore.
 | 
				
			||||||
@@ -107,7 +135,6 @@ def wait_while_manipulation_running(
 | 
				
			|||||||
                break
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if any_active:
 | 
					        if any_active:
 | 
				
			||||||
            # Check timeout
 | 
					 | 
				
			||||||
            elapsed = time.time() - start
 | 
					            elapsed = time.time() - start
 | 
				
			||||||
            if timeout and elapsed >= timeout:
 | 
					            if timeout and elapsed >= timeout:
 | 
				
			||||||
                print(f"Timeout ({timeout}s) reached while waiting for services. Continuing anyway.")
 | 
					                print(f"Timeout ({timeout}s) reached while waiting for services. Continuing anyway.")
 | 
				
			||||||
@@ -119,7 +146,30 @@ def wait_while_manipulation_running(
 | 
				
			|||||||
            break
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main(base_directory: str, manipulation_services: List[str], timeout: int | None) -> int:
 | 
					def get_compose_project_info(container: str) -> Tuple[str, str]:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Resolve project name and working dir from Docker labels.
 | 
				
			||||||
 | 
					    STRICT: Raises RuntimeError if labels are missing/unreadable.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    out_project = print_bash(
 | 
				
			||||||
 | 
					        f"docker inspect -f '{{{{ index .Config.Labels \"com.docker.compose.project\" }}}}' {container}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    out_workdir = print_bash(
 | 
				
			||||||
 | 
					        f"docker inspect -f '{{{{ index .Config.Labels \"com.docker.compose.project.working_dir\" }}}}' {container}"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    project = out_project[0].strip() if out_project else ""
 | 
				
			||||||
 | 
					    workdir = out_workdir[0].strip() if out_workdir else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not project:
 | 
				
			||||||
 | 
					        raise RuntimeError(f"No compose project label found for container {container}")
 | 
				
			||||||
 | 
					    if not workdir:
 | 
				
			||||||
 | 
					        raise RuntimeError(f"No compose working_dir label found for container {container}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return project, workdir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main(base_directory: str, manipulation_services: List[str], timeout: Optional[int]) -> int:
 | 
				
			||||||
    errors = 0
 | 
					    errors = 0
 | 
				
			||||||
    wait_while_manipulation_running(manipulation_services, waiting_time=600, timeout=timeout)
 | 
					    wait_while_manipulation_running(manipulation_services, waiting_time=600, timeout=timeout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -131,43 +181,50 @@ def main(base_directory: str, manipulation_services: List[str], timeout: int | N
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    failed_containers = unhealthy_container_names + exited_container_names
 | 
					    failed_containers = unhealthy_container_names + exited_container_names
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    unfiltered_failed_docker_compose_repositories = [
 | 
					    for container in failed_containers:
 | 
				
			||||||
        container.split("-")[0] for container in failed_containers
 | 
					        try:
 | 
				
			||||||
    ]
 | 
					            project, workdir = get_compose_project_info(container)
 | 
				
			||||||
    filtered_failed_docker_compose_repositories = list(
 | 
					        except Exception as e:
 | 
				
			||||||
        dict.fromkeys(unfiltered_failed_docker_compose_repositories)
 | 
					            print(f"Error reading compose labels for {container}: {e}")
 | 
				
			||||||
    )
 | 
					            errors += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for repo in filtered_failed_docker_compose_repositories:
 | 
					        compose_file_path = os.path.join(workdir, "docker-compose.yml")
 | 
				
			||||||
        compose_file_path = find_docker_compose_file(os.path.join(base_directory, repo))
 | 
					        if not os.path.isfile(compose_file_path):
 | 
				
			||||||
 | 
					            # As STRICT: we only trust labels; if file not there, error out.
 | 
				
			||||||
 | 
					            print(f"Error: docker-compose.yml not found at {compose_file_path} for container {container}")
 | 
				
			||||||
 | 
					            errors += 1
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if compose_file_path:
 | 
					 | 
				
			||||||
            print("Restarting unhealthy container in:", compose_file_path)
 | 
					 | 
				
			||||||
        project_path = os.path.dirname(compose_file_path)
 | 
					        project_path = os.path.dirname(compose_file_path)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
                # restart with optional --env-file and -p
 | 
					            print("Restarting unhealthy container in:", compose_file_path)
 | 
				
			||||||
                print_bash(compose_cmd("restart", project_path, repo))
 | 
					            print_bash(compose_cmd("restart", project_path, project))
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            if "port is already allocated" in str(e):
 | 
					            if "port is already allocated" in str(e):
 | 
				
			||||||
                print("Detected port allocation problem. Executing recovery steps...")
 | 
					                print("Detected port allocation problem. Executing recovery steps...")
 | 
				
			||||||
                    # down (no -p needed), then engine restart, then up -d with -p
 | 
					                try:
 | 
				
			||||||
                    print_bash(compose_cmd("down", project_path))
 | 
					                    print_bash(compose_cmd("down", project_path))
 | 
				
			||||||
                    print_bash("systemctl restart docker")
 | 
					                    print_bash("systemctl restart docker")
 | 
				
			||||||
                    print_bash(compose_cmd("up -d", project_path, repo))
 | 
					                    print_bash(compose_cmd("up -d", project_path, project))
 | 
				
			||||||
                else:
 | 
					                except Exception as e2:
 | 
				
			||||||
                    print("Unhandled exception during restart:", e)
 | 
					                    print("Unhandled exception during recovery:", e2)
 | 
				
			||||||
                    errors += 1
 | 
					                    errors += 1
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
            print("Error: Docker Compose file not found for:", repo)
 | 
					                print("Unhandled exception during restart:", e)
 | 
				
			||||||
                errors += 1
 | 
					                errors += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    print("Finished restart procedure.")
 | 
					    print("Finished restart procedure.")
 | 
				
			||||||
    return errors
 | 
					    return errors
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					# CLI
 | 
				
			||||||
 | 
					# ---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    parser = argparse.ArgumentParser(
 | 
					    parser = argparse.ArgumentParser(
 | 
				
			||||||
        description="Restart Docker-Compose configurations with exited or unhealthy containers."
 | 
					        description="Restart Docker-Compose configurations with exited or unhealthy containers (STRICT label mode)."
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        "--manipulation",
 | 
					        "--manipulation",
 | 
				
			||||||
@@ -189,7 +246,7 @@ if __name__ == "__main__":
 | 
				
			|||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        "base_directory",
 | 
					        "base_directory",
 | 
				
			||||||
        type=str,
 | 
					        type=str,
 | 
				
			||||||
        help="Base directory where Docker Compose configurations are located.",
 | 
					        help="(Unused in STRICT mode) Base directory where Docker Compose configurations are located.",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    args = parser.parse_args()
 | 
					    args = parser.parse_args()
 | 
				
			||||||
    services = normalize_services_arg(args.manipulation, args.manipulation_string)
 | 
					    services = normalize_services_arg(args.manipulation, args.manipulation_string)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,13 +17,7 @@ When enabled via `MODE_CLEANUP` or `MODE_RESET`, it will automatically prune unu
 | 
				
			|||||||
  Installs Docker and Docker Compose via the system package manager.
 | 
					  Installs Docker and Docker Compose via the system package manager.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **Integrated Dependencies**  
 | 
					- **Integrated Dependencies**  
 | 
				
			||||||
  Includes backup, repair, and health check sub-roles:
 | 
					  Includes backup, repair, and health check sub-roles
 | 
				
			||||||
  - `sys-ctl-bkp-docker-2-loc`
 | 
					 | 
				
			||||||
  - `user-administrator`
 | 
					 | 
				
			||||||
  - `sys-ctl-hlth-docker-container`
 | 
					 | 
				
			||||||
  - `sys-ctl-hlth-docker-volumes`
 | 
					 | 
				
			||||||
  - `sys-ctl-rpr-docker-soft`
 | 
					 | 
				
			||||||
  - `sys-ctl-rpr-docker-hard`
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
- **Cleanup & Reset Modes**  
 | 
					- **Cleanup & Reset Modes**  
 | 
				
			||||||
  - `MODE_CLEANUP`: Removes unused Docker containers, networks, images, and volumes.  
 | 
					  - `MODE_CLEANUP`: Removes unused Docker containers, networks, images, and volumes.  
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,5 @@
 | 
				
			|||||||
    - sys-ctl-bkp-docker-2-loc
 | 
					    - sys-ctl-bkp-docker-2-loc
 | 
				
			||||||
    - sys-ctl-hlth-docker-container
 | 
					    - sys-ctl-hlth-docker-container
 | 
				
			||||||
    - sys-ctl-hlth-docker-volumes
 | 
					    - sys-ctl-hlth-docker-volumes
 | 
				
			||||||
    - sys-ctl-rpr-docker-soft
 | 
					 | 
				
			||||||
    - sys-ctl-rpr-docker-hard
 | 
					    - sys-ctl-rpr-docker-hard
 | 
				
			||||||
  when: SYS_SVC_DOCKER_LOAD_SERVICES | bool
 | 
					  when: SYS_SVC_DOCKER_LOAD_SERVICES | bool
 | 
				
			||||||
@@ -115,29 +115,48 @@ class TestRepairDockerSoft(unittest.TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        def fake_print_bash(cmd):
 | 
					        def fake_print_bash(cmd):
 | 
				
			||||||
            cmd_log.append(cmd)
 | 
					            cmd_log.append(cmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # 1) docker ps Mocks (deine bisherigen)
 | 
				
			||||||
            if cmd.startswith("docker ps --filter health=unhealthy"):
 | 
					            if cmd.startswith("docker ps --filter health=unhealthy"):
 | 
				
			||||||
                return ["app1-web-1", "db-1"]
 | 
					                return ["app1-web-1", "db-1"]
 | 
				
			||||||
            if cmd.startswith("docker ps --filter status=exited"):
 | 
					            if cmd.startswith("docker ps --filter status=exited"):
 | 
				
			||||||
                return ["app1-worker-1", "other-2"]
 | 
					                return ["app1-worker-1", "other-2"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # 2) docker inspect Labels (NEU)
 | 
				
			||||||
 | 
					            # project label
 | 
				
			||||||
 | 
					            if cmd.startswith("docker inspect -f '{{ index .Config.Labels \"com.docker.compose.project\" }}'"):
 | 
				
			||||||
 | 
					                container = cmd.split()[-1]
 | 
				
			||||||
 | 
					                if container in ("app1-web-1", "app1-worker-1"):
 | 
				
			||||||
 | 
					                    return ["app1"]
 | 
				
			||||||
 | 
					                if container == "db-1":
 | 
				
			||||||
 | 
					                    return ["db"]
 | 
				
			||||||
 | 
					                return [""]  # other-2 hat keine Labels -> soll fehlschlagen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # working_dir label
 | 
				
			||||||
 | 
					            if cmd.startswith("docker inspect -f '{{ index .Config.Labels \"com.docker.compose.project.working_dir\" }}'"):
 | 
				
			||||||
 | 
					                container = cmd.split()[-1]
 | 
				
			||||||
 | 
					                if container in ("app1-web-1", "app1-worker-1"):
 | 
				
			||||||
 | 
					                    return ["/BASE/app1"]
 | 
				
			||||||
 | 
					                if container == "db-1":
 | 
				
			||||||
 | 
					                    return ["/BASE/db"]
 | 
				
			||||||
 | 
					                return [""]  # other-2 -> keine Angabe
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # 3) docker-compose Aufrufe (unverändert okay)
 | 
				
			||||||
            if "docker-compose" in cmd:
 | 
					            if "docker-compose" in cmd:
 | 
				
			||||||
                return []
 | 
					                return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return []
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # find_docker_compose_file wird in STRICT nicht benutzt, kann aber bleiben
 | 
				
			||||||
        def fake_find_docker_compose(path):
 | 
					        def fake_find_docker_compose(path):
 | 
				
			||||||
            # Compose-Projekte: app1, db -> vorhanden; "other" -> nicht vorhanden
 | 
					 | 
				
			||||||
            if path.endswith("/app1") or path.endswith("/db"):
 | 
					            if path.endswith("/app1") or path.endswith("/db"):
 | 
				
			||||||
                return str(Path(path) / "docker-compose.yml")
 | 
					                return str(Path(path) / "docker-compose.yml")
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Steuere die detect_env_file-Antwort:
 | 
					        # 4) os.path.isfile für STRICT mode (NEU)
 | 
				
			||||||
        # - Für app1 existiert nur .env/env
 | 
					        old_isfile = s.os.path.isfile
 | 
				
			||||||
        # - Für db existiert .env
 | 
					        def fake_isfile(path):
 | 
				
			||||||
        def fake_detect_env_file(project_path: str):
 | 
					            return path in ("/BASE/app1/docker-compose.yml", "/BASE/db/docker-compose.yml")
 | 
				
			||||||
            if project_path.endswith("/app1"):
 | 
					 | 
				
			||||||
                return f"{project_path}/.env/env"
 | 
					 | 
				
			||||||
            if project_path.endswith("/db"):
 | 
					 | 
				
			||||||
                return f"{project_path}/.env"
 | 
					 | 
				
			||||||
            return None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        old_print_bash = s.print_bash
 | 
					        old_print_bash = s.print_bash
 | 
				
			||||||
        old_find = s.find_docker_compose_file
 | 
					        old_find = s.find_docker_compose_file
 | 
				
			||||||
@@ -145,14 +164,18 @@ class TestRepairDockerSoft(unittest.TestCase):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            s.print_bash = fake_print_bash
 | 
					            s.print_bash = fake_print_bash
 | 
				
			||||||
            s.find_docker_compose_file = fake_find_docker_compose
 | 
					            s.find_docker_compose_file = fake_find_docker_compose
 | 
				
			||||||
            s.detect_env_file = fake_detect_env_file
 | 
					            s.detect_env_file = lambda project_path: (
 | 
				
			||||||
 | 
					                f"{project_path}/.env/env" if project_path.endswith("/app1")
 | 
				
			||||||
 | 
					                else (f"{project_path}/.env" if project_path.endswith("/db") else None)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            s.os.path.isfile = fake_isfile  # <— wichtig für STRICT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            errors = s.main("/BASE", manipulation_services=[], timeout=None)
 | 
					            errors = s.main("/BASE", manipulation_services=[], timeout=None)
 | 
				
			||||||
            # one error expected for "other" (no compose file)
 | 
					
 | 
				
			||||||
 | 
					            # Erwartung: nur "other-2" scheitert -> 1 Fehler
 | 
				
			||||||
            self.assertEqual(errors, 1)
 | 
					            self.assertEqual(errors, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            restart_cmds = [c for c in cmd_log if ' docker-compose' in c and " restart" in c]
 | 
					            restart_cmds = [c for c in cmd_log if ' docker-compose' in c and " restart" in c]
 | 
				
			||||||
            # app1: --env-file "/BASE/app1/.env/env" + -p "app1"
 | 
					 | 
				
			||||||
            self.assertTrue(any(
 | 
					            self.assertTrue(any(
 | 
				
			||||||
                'cd "/BASE/app1"' in c and
 | 
					                'cd "/BASE/app1"' in c and
 | 
				
			||||||
                '--env-file "/BASE/app1/.env/env"' in c and
 | 
					                '--env-file "/BASE/app1/.env/env"' in c and
 | 
				
			||||||
@@ -160,7 +183,6 @@ class TestRepairDockerSoft(unittest.TestCase):
 | 
				
			|||||||
                ' restart' in c
 | 
					                ' restart' in c
 | 
				
			||||||
                for c in restart_cmds
 | 
					                for c in restart_cmds
 | 
				
			||||||
            ))
 | 
					            ))
 | 
				
			||||||
            # db: --env-file "/BASE/db/.env" + -p "db"
 | 
					 | 
				
			||||||
            self.assertTrue(any(
 | 
					            self.assertTrue(any(
 | 
				
			||||||
                'cd "/BASE/db"' in c and
 | 
					                'cd "/BASE/db"' in c and
 | 
				
			||||||
                '--env-file "/BASE/db/.env"' in c and
 | 
					                '--env-file "/BASE/db/.env"' in c and
 | 
				
			||||||
@@ -172,6 +194,8 @@ class TestRepairDockerSoft(unittest.TestCase):
 | 
				
			|||||||
            s.print_bash = old_print_bash
 | 
					            s.print_bash = old_print_bash
 | 
				
			||||||
            s.find_docker_compose_file = old_find
 | 
					            s.find_docker_compose_file = old_find
 | 
				
			||||||
            s.detect_env_file = old_detect
 | 
					            s.detect_env_file = old_detect
 | 
				
			||||||
 | 
					            s.os.path.isfile = old_isfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user