"""Docker build service for building images from Dockerfiles.""" import logging import subprocess logger = logging.getLogger(__name__) def build_image( instance_dir: str, dockerfile: str, tag: str, build_context: dict | None = None ) -> tuple[int, str, str]: """Build a Docker image from a Dockerfile. Args: instance_dir: Directory containing the Dockerfile dockerfile: Dockerfile content tag: Image tag to apply build_context: Optional build context files {path: content} Returns: Tuple of (returncode, stdout, stderr) """ from pathlib import Path # Defensive: normalise any CRLF that may have crept in from manifest DB # strings — Docker's legacy builder treats \r as a character after the # backslash, breaking RUN continuations and producing # "unknown instruction" errors. dockerfile = dockerfile.replace("\r\n", "\n").replace("\r", "\n") # Write Dockerfile dockerfile_path = Path(instance_dir) / "Dockerfile" dockerfile_path.write_text(dockerfile, newline="\n") logger.debug("Wrote Dockerfile to %s (%d bytes)", dockerfile_path, len(dockerfile)) # Write build context files if build_context: for file_path, content in build_context.items(): full_path = Path(instance_dir) / file_path # Security: ensure path doesn't escape instance_dir try: full_path.resolve().relative_to(Path(instance_dir).resolve()) except ValueError: logger.error( "Build context file path escapes instance directory: %s", file_path ) raise ValueError( f"Build context file path '{file_path}' escapes instance directory" ) full_path.parent.mkdir(parents=True, exist_ok=True) normalized = content.replace("\r\n", "\n").replace("\r", "\n") full_path.write_text(normalized, newline="\n") logger.debug("Wrote build context file: %s", full_path) # Build image logger.debug("Building Docker image with tag: %s", tag) cmd = [ "docker", "build", "-t", tag, "-f", str(dockerfile_path), instance_dir, ] try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=300, # 5 minute timeout for builds ) logger.debug("Docker build completed: returncode=%d", result.returncode) if result.returncode != 0: logger.error("Docker build failed: %s", result.stderr[:1000]) return result.returncode, result.stdout, result.stderr except subprocess.TimeoutExpired: logger.error("Docker build timed out after 300 seconds") return 1, "", "Build timed out after 300 seconds" except Exception as exc: logger.exception("Docker build failed: %s", exc) return 1, "", str(exc)