from pathlib import Path import subprocess from ollama_client import ask import yaml def _build_prompt(service: str, extra: str = "") -> str: prompt = ( "Output ONLY valid docker-compose YAML. No explanations. No markdown.\n\n" f"Generate a docker-compose file for this service: {service}" ) if extra: prompt = f"{prompt}\n{extra}" return prompt def _generate_compose(service: str, extra: str = "") -> str: base_prompt = ( _build_prompt(service, extra) ) retry_prompt = ( "Previous output was invalid. Fix YAML and return valid docker-compose only." ) prompt = base_prompt for _ in range(3): response = ask(prompt) if response.startswith("ERROR:"): return response try: parsed = yaml.safe_load(response) except yaml.YAMLError: prompt = f"{base_prompt}\n\n{retry_prompt}" continue if isinstance(parsed, dict) and "services" in parsed: return response prompt = f"{base_prompt}\n\n{retry_prompt}" return "ERROR: invalid docker-compose" def generate_compose(service: str) -> str: return _generate_compose(service) def get_service_status(path: Path) -> str: try: result = subprocess.run( ["docker", "compose", "ps"], cwd=path, check=False, capture_output=True, text=True, ) except Exception as exc: return f"ERROR: {exc}" output = result.stdout.strip() if result.returncode != 0: error = result.stderr.strip() or output or "docker compose ps failed" return f"ERROR: {error}" return output def deploy_service(service: str) -> str: prompt_extra = "" try: docker_ps = subprocess.run( ["docker", "ps"], check=False, capture_output=True, text=True, ) docker_ps_output = docker_ps.stdout if ":80->" in docker_ps_output or "0.0.0.0:80-" in docker_ps_output or "[::]:80-" in docker_ps_output: prompt_extra = "Use a different port than 80" except Exception: docker_ps_output = "" compose = _generate_compose(service, prompt_extra) if compose.startswith("ERROR:"): return compose deployments_dir = Path("./deployments") target_dir = deployments_dir / service suffix = 1 while target_dir.exists(): target_dir = deployments_dir / f"{service}-{suffix}" suffix += 1 target_dir.mkdir(parents=True, exist_ok=False) compose_path = target_dir / "docker-compose.yml" compose_path.write_text(compose, encoding="utf-8") try: subprocess.run( ["docker", "compose", "up", "-d"], cwd=target_dir, check=True, capture_output=True, text=True, ) except subprocess.CalledProcessError as exc: message = exc.stderr.strip() or exc.stdout.strip() or str(exc) return f"ERROR: {message}" except Exception as exc: return f"ERROR: {exc}" status = get_service_status(target_dir) if status.startswith("ERROR:"): return status if "Up" not in status: return f"ERROR: no running services\n{status}" return f"DEPLOYED: {target_dir.name}\n{status}" if __name__ == "__main__": print(deploy_service("nginx"))