Team Ai
Apppublic

jester1177/cloud-native-debug-env

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
docker_simulator.py254 linesDownload Raw Back to simulators
1"""Docker build/run simulator — deterministic, rule-based."""2 3from typing import Dict, List, Optional, Set4 5from server.models import FileContent6 7 8class DockerSimulator:9    VALID_INSTRUCTIONS: Set[str] = {10        "FROM",11        "RUN",12        "CMD",13        "LABEL",14        "MAINTAINER",15        "EXPOSE",16        "ENV",17        "ADD",18        "COPY",19        "ENTRYPOINT",20        "VOLUME",21        "USER",22        "WORKDIR",23        "ARG",24        "ONBUILD",25        "STOPSIGNAL",26        "HEALTHCHECK",27        "SHELL",28    }29 30    def _split_lines(self, content: str) -> List[str]:31        return [line.rstrip() for line in content.split("\n")]32 33    def _non_empty_non_comment_lines(self, lines: List[str]) -> List[str]:34        return [line.strip() for line in lines if line.strip() and not line.strip().startswith("#")]35 36    def _source_exists(self, source: str, context_files: Dict[str, FileContent]) -> bool:37        if source in {".", "./"}:38            return True39        if "*" in source:40            prefix = source.replace("*", "")41            return any(path.startswith(prefix) for path in context_files)42        return source in context_files43 44    def _join_continuation_lines(self, lines: List[str]) -> List[str]:45        """Join lines ending with backslash into single logical lines."""46        result: List[str] = []47        current = ""48        for line in lines:49            stripped = line.rstrip()50            if stripped.endswith("\\"):51                current += stripped[:-1] + " "52            else:53                current += stripped54                result.append(current)55                current = ""56        if current:57            result.append(current)58        return result59 60    def validate(self, dockerfile: Optional[FileContent], context_files: Dict[str, FileContent]):61        if dockerfile is None:62            return {"build_success": False, "run_success": False, "error": "Dockerfile missing"}63 64        content = dockerfile.content65        lines = self._split_lines(content)66        active_lines = self._non_empty_non_comment_lines(lines)67 68        if not active_lines:69            return {"build_success": False, "run_success": False, "error": "Dockerfile is empty"}70 71        # ARG before FROM is fine, but the first real instruction must be FROM72        first_non_arg = None73        for line in active_lines:74            token = line.split()[0].upper()75            if token == "ARG":76                continue77            first_non_arg = token78            break79 80        if first_non_arg is None or first_non_arg != "FROM":81            return {82                "build_success": False,83                "run_success": False,84                "error": "Dockerfile must start with FROM",85            }86 87        # validate instructions88        for idx, raw in enumerate(active_lines, start=1):89            token = raw.split()[0].upper()90            # Handle --platform= prefix on FROM91            if token.startswith("FROM"):92                token = "FROM"93            if token.startswith("&&"):94                return {95                    "build_success": False,96                    "run_success": False,97                    "error": f"Dockerfile parse error: unknown instruction: {token}",98                    "line": idx,99                }100            # Strip leading --flags (e.g. --platform=...) — the instruction is after101            if token.startswith("--"):102                continue103            if token not in self.VALID_INSTRUCTIONS:104                return {105                    "build_success": False,106                    "run_success": False,107                    "error": f"Dockerfile parse error: unknown instruction: {token}",108                    "line": idx,109                }110 111        # known-bad base image tags112        if "FROM python:3.9-slimm" in content:113            return {114                "build_success": False,115                "run_success": False,116                "error": "pull access denied for python:3.9-slimm",117            }118 119        # typo in requirements filename120        if "requirments.txt" in content:121            return {122                "build_success": False,123                "run_success": False,124                "error": "COPY failed: file not found in build context: requirments.txt",125            }126 127        # COPY source must exist in build context128        for raw in active_lines:129            upper = raw.upper()130            if upper.startswith("COPY "):131                parts = raw.split()132                if len(parts) < 3:133                    return {134                        "build_success": False,135                        "run_success": False,136                        "error": "COPY requires source and destination",137                    }138                src = parts[1]139                if src.startswith("--from=") and len(parts) >= 4:140                    src = parts[2]141                if src.startswith("--"):142                    continue143                if not self._source_exists(src, context_files):144                    return {145                        "build_success": False,146                        "run_success": False,147                        "error": f"COPY failed: file not found in build context: {src}",148                    }149 150        # platform ARGs need to be declared151        if "--platform=$BUILDPLATFORM" in content and "ARG BUILDPLATFORM" not in content:152            return {153                "build_success": False,154                "run_success": False,155                "error": "failed to parse platform: BUILDPLATFORM not declared",156            }157        if "--platform=$TARGETPLATFORM" in content and "ARG TARGETPLATFORM" not in content:158            return {159                "build_success": False,160                "run_success": False,161                "error": "failed to parse platform: TARGETPLATFORM not declared",162            }163 164        # multi-stage: output dir mismatch (dist vs build)165        if "COPY --from=builder /app/dist" in content:166            pkg = context_files.get("package.json")167            if pkg and "react-scripts build" in pkg.content:168                return {169                    "build_success": False,170                    "run_success": False,171                    "error": "COPY failed: stat app/dist: file does not exist",172                }173 174        # EXPOSE must have a numeric port175        for raw in active_lines:176            upper = raw.upper()177            if upper.startswith("EXPOSE "):178                parts = raw.split()179                for part in parts[1:]:180                    cleaned = part.strip('"').strip("'")181                    port_proto = cleaned.split("/")[0]182                    if not port_proto.isdigit():183                        return {184                            "build_success": False,185                            "run_success": False,186                            "error": f"EXPOSE requires numeric port or port/protocol, got: {cleaned}",187                        }188 189        # ============================190        # runtime checks (build OK, run might fail)191        # ============================192 193        # no WORKDIR → module resolution fails194        has_workdir = "WORKDIR" in content195        if ("npm start" in content or 'CMD ["npm", "start"]' in content) and not has_workdir:196            return {197                "build_success": True,198                "run_success": False,199                "run_error": "Error: Cannot find module '/package.json'",200            }201 202        # ENTRYPOINT + CMD both specify python → conflict203        if 'ENTRYPOINT ["python"' in content and 'CMD ["python"' in content:204            return {205                "build_success": True,206                "run_success": False,207                "run_error": "container exits immediately; ENTRYPOINT and CMD both specify full command",208            }209 210        # entrypoint script needs chmod +x211        if 'ENTRYPOINT ["./start.sh"]' in content and "chmod +x" not in content:212            return {213                "build_success": True,214                "run_success": False,215                "run_error": "exec ./start.sh: permission denied",216            }217 218        # DATABASE_URL env var missing219        has_database_url_env = "ENV DATABASE_URL" in content220        needs_database_url = (221            "app.py" in content222            and "DATABASE_URL" not in content223            and any("gunicorn" in fc.content for fc in context_files.values() if fc.content)224        )225        if needs_database_url and not has_database_url_env:226            return {227                "build_success": True,228                "run_success": False,229                "run_error": "KeyError: 'DATABASE_URL' — Application requires DATABASE_URL environment variable",230            }231 232        # non-root user can't bind privileged ports233        has_user_switch = False234        expose_port = None235        for raw in active_lines:236            upper = raw.upper()237            if upper.startswith("USER ") and "root" not in raw.lower():238                has_user_switch = True239            if upper.startswith("EXPOSE "):240                parts = raw.split()241                if len(parts) >= 2:242                    port_str = parts[1].split("/")[0].strip('"').strip("'")243                    if port_str.isdigit():244                        expose_port = int(port_str)245 246        if has_user_switch and expose_port is not None and expose_port < 1024:247            return {248                "build_success": True,249                "run_success": False,250                "run_error": f"PermissionError: [Errno 13] Permission denied — non-root user cannot bind to port {expose_port}",251            }252 253        return {"build_success": True, "run_success": True}254