From 26d345575439eec26dbf3c114f0583e7349c7ec3 Mon Sep 17 00:00:00 2001 From: raphgm Date: Wed, 23 Sep 2026 22:13:32 +0100 Subject: [PATCH 01/23] feat: add preflight, joint project/host failure prediction Preflight parses Compose files and Dockerfiles with Docker's own parsers, profiles the daemon host (including the Colima VM kernel), reads image platforms and sizes from registries without pulling, and predicts build/run failures before anything executes. Checks blocked by an unknown fact are grouped under that root cause. Also adds snapshot/--host to predict for another machine, validation notes from a real Colima run, and fixes the CI Go version. --- .github/workflows/ci.yml | 2 +- README.md | 81 ++- cmd/preflight.go | 194 ++++++ cmd/root.go | 18 +- cmd/snapshot.go | 45 ++ docs/validation/2026-09-23-colima-m-series.md | 50 ++ examples/broken-shop/compose.yaml | 35 ++ examples/broken-shop/web/.dockerignore | 2 + examples/broken-shop/web/.env.production | 0 examples/broken-shop/web/Dockerfile | 11 + examples/broken-shop/web/package.json | 1 + examples/broken-shop/web/src/index.js | 1 + examples/hosts/macbook-m2-desktop.json | 28 + examples/hosts/ubuntu-legacy-server.json | 27 + go.mod | 32 +- go.sum | 80 ++- pkg/preflight/fact/fact.go | 111 ++++ pkg/preflight/fact/fact_test.go | 39 ++ pkg/preflight/host/diagnose.go | 157 +++++ pkg/preflight/host/diagnose_test.go | 63 ++ pkg/preflight/host/host.go | 553 ++++++++++++++++++ pkg/preflight/host/host_test.go | 127 ++++ pkg/preflight/host/snapshot.go | 123 ++++ pkg/preflight/preflight.go | 199 +++++++ pkg/preflight/preflight_test.go | 128 ++++ pkg/preflight/project/context.go | 104 ++++ pkg/preflight/project/dockerfile.go | 275 +++++++++ pkg/preflight/project/load.go | 288 +++++++++ pkg/preflight/project/project.go | 128 ++++ pkg/preflight/project/project_test.go | 240 ++++++++ pkg/preflight/project/scan.go | 142 +++++ pkg/preflight/registry/live_test.go | 63 ++ pkg/preflight/registry/registry.go | 256 ++++++++ pkg/preflight/rules/build.go | 250 ++++++++ pkg/preflight/rules/host_io.go | 220 +++++++ pkg/preflight/rules/images.go | 211 +++++++ pkg/preflight/rules/resources.go | 309 ++++++++++ pkg/preflight/rules/rule.go | 187 ++++++ pkg/preflight/rules/rules_test.go | 399 +++++++++++++ 39 files changed, 5148 insertions(+), 31 deletions(-) create mode 100644 cmd/preflight.go create mode 100644 cmd/snapshot.go create mode 100644 docs/validation/2026-09-23-colima-m-series.md create mode 100644 examples/broken-shop/compose.yaml create mode 100644 examples/broken-shop/web/.dockerignore create mode 100644 examples/broken-shop/web/.env.production create mode 100644 examples/broken-shop/web/Dockerfile create mode 100644 examples/broken-shop/web/package.json create mode 100644 examples/broken-shop/web/src/index.js create mode 100644 examples/hosts/macbook-m2-desktop.json create mode 100644 examples/hosts/ubuntu-legacy-server.json create mode 100644 pkg/preflight/fact/fact.go create mode 100644 pkg/preflight/fact/fact_test.go create mode 100644 pkg/preflight/host/diagnose.go create mode 100644 pkg/preflight/host/diagnose_test.go create mode 100644 pkg/preflight/host/host.go create mode 100644 pkg/preflight/host/host_test.go create mode 100644 pkg/preflight/host/snapshot.go create mode 100644 pkg/preflight/preflight.go create mode 100644 pkg/preflight/preflight_test.go create mode 100644 pkg/preflight/project/context.go create mode 100644 pkg/preflight/project/dockerfile.go create mode 100644 pkg/preflight/project/load.go create mode 100644 pkg/preflight/project/project.go create mode 100644 pkg/preflight/project/project_test.go create mode 100644 pkg/preflight/project/scan.go create mode 100644 pkg/preflight/registry/live_test.go create mode 100644 pkg/preflight/registry/registry.go create mode 100644 pkg/preflight/rules/build.go create mode 100644 pkg/preflight/rules/host_io.go create mode 100644 pkg/preflight/rules/images.go create mode 100644 pkg/preflight/rules/resources.go create mode 100644 pkg/preflight/rules/rule.go create mode 100644 pkg/preflight/rules/rules_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 733348d..a88400c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Verify dependencies run: go mod verify diff --git a/README.md b/README.md index 2dcc8b8..b820db1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,81 @@ # Container Doctor 🩺 -`container-doctor` is a fast, extensible CLI tool designed to diagnose your system's readiness for running Docker and container workloads. It performs health checks across your system, Docker daemon, and Compose installation, alerting you to critical issues like low memory, insufficient disk space, or missing dependencies. +`container-doctor` predicts whether a container project will build and run on a machine **before you run it**, and explains each predicted failure once, at its root cause. -## Features +Linters look only at your Dockerfile. `docker info` looks only at your machine. Most "works on my machine" failures come from the mismatch between the two: an amd64-only image on an Apple silicon laptop, a Compose file that needs more memory than Docker Desktop's VM has, a `COPY` of a file your `.dockerignore` excludes, a port something else already holds. `container-doctor preflight` checks the project and the host together. + +## Preflight + +```bash +container-doctor preflight . +``` + +Preflight builds three views and joins them: + +1. **What the project needs.** The Compose file and every Dockerfile are parsed with Docker's own parsers (`compose-go`, BuildKit's Dockerfile frontend). This covers platforms, memory limits and replicas, published ports, bind mounts, `COPY` sources (only in stages BuildKit will actually build), BuildKit-only syntax, Compose features and interpolated variables. +2. **What the host offers.** The daemon's platform and memory (on macOS and Windows these are the VM's, not the laptop's), emulation, Compose/buildx versions, kernel settings, local images, credential helpers and live port usage. +3. **What registries publish.** Each image's platforms and download size, read from the registry without pulling. + +Each finding quotes its evidence from both sides and the error Docker would print: + +``` +✗ [web] COPY source ".env.production" is excluded by .dockerignore Dockerfile:11 + web/.dockerignore: pattern ".env*" + would fail with: "failed to compute cache key: \"/.env.production\": not found" + fix: Remove or negate the pattern ".env*" in web/.dockerignore. +``` + +### Root causes, not symptom lists + +Checks depend on facts: the Docker CLI → the daemon → its platform and memory, and so on. When a fact cannot be established, every check that needs it is grouped under that single root cause instead of failing separately. Checks that do not depend on it keep running. + +``` +ROOT CAUSES + ✗ Docker daemon is not reachable + fix: Start Docker Desktop (or `colima start`, `sudo systemctl start docker`) … + blocks 5 check(s): image.platform, resources.memory, resources.gpu, … +``` + +### Predict for another machine + +```bash +# on the target machine (a teammate's laptop, a CI runner, a server) +container-doctor snapshot -o ci-runner.json + +# anywhere +container-doctor preflight . --host ci-runner.json +``` + +`examples/` contains a deliberately broken project and two example host snapshots. The same project gets different predictions on each: + +```bash +container-doctor preflight examples/broken-shop --host examples/hosts/macbook-m2-desktop.json +container-doctor preflight examples/broken-shop --host examples/hosts/ubuntu-legacy-server.json +``` + +### Rules + +| Rule | Predicts | +|---|---| +| `image.exists` | tag or repository missing from the registry | +| `image.platform` | no variant for the host's platform, emulation needed or unavailable (`exec format error`) | +| `registry.credentials` | configured credential helper not installed (`error getting credentials`) | +| `build.context` | `COPY`/`ADD` sources missing, excluded by `.dockerignore`, or outside the context; bad target or ARG | +| `build.buildkit` | `RUN --mount`, heredocs, `COPY --chmod/--link` without BuildKit | +| `compose.version` | Compose features newer than the installed Compose | +| `compose.env` | unset variables, required `${VAR:?}` without a value | +| `resources.memory` | known image minimums and total limits vs Docker's memory | +| `resources.disk` | image download size ×2.5 vs free disk | +| `resources.gpu` | GPU requests without the NVIDIA runtime, or on macOS | +| `kernel.sysctl` | `vm.max_map_count` for Elasticsearch/OpenSearch/SonarQube | +| `ports.conflict` | two services, or several replicas, on one host port | +| `ports.host` | host port already held by another process or container; privileged ports under rootless Docker | +| `mounts.bind` | missing bind sources that Docker will turn into directories, stale empty directories | +| `mounts.sharing` | paths outside Docker Desktop's shared folders | + +Use `--format json` for machine-readable output and `--offline` to skip registry lookups. The exit code is 1 when a blocking problem is predicted. + +## Other commands - **System Diagnostics**: Checks your operating system, CPU architecture, memory (RAM), and disk space to ensure you meet minimum requirements. - **Docker Health**: Validates that Docker is installed, the daemon is running, the socket is accessible, and client/server versions are compatible. @@ -28,7 +101,7 @@ Head over to the [Releases page](https://github.com/raphgm/container-doctor/rele ## Usage -### 1. Run Health Checks +### Host health checks Run the diagnostic engine to check your environment: ```bash container-doctor check @@ -46,7 +119,7 @@ container-doctor check --format markdown > report.md container-doctor check --format html > report.html ``` -### 2. Inspect a Project +### Inspect a project Statically analyze the configuration files in your current directory (or any specified path): ```bash container-doctor inspect . diff --git a/cmd/preflight.go b/cmd/preflight.go new file mode 100644 index 0000000..cdbf14f --- /dev/null +++ b/cmd/preflight.go @@ -0,0 +1,194 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/raphgm/container-doctor/pkg/preflight" + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/host" +) + +var ( + preflightFormat string + preflightOffline bool + preflightHost string +) + +var preflightCmd = &cobra.Command{ + Use: "preflight [path]", + Short: "Predict whether a project will build and run on this machine", + Long: `Preflight reads the project's Compose file and Dockerfiles, profiles this +host (the Docker daemon, its VM, tooling, ports, kernel settings) and queries +registries for image platforms and sizes. It then predicts the failures +` + "`docker compose up`" + ` would hit, before anything runs. + +Checks that cannot run because something upstream is broken are grouped +under that root cause instead of being reported as separate failures.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + dir := "." + if len(args) > 0 { + dir = args[0] + } + opts := preflight.Options{Dir: dir, Offline: preflightOffline} + if preflightHost != "" { + snap, err := host.LoadSnapshot(preflightHost) + if err != nil { + return err + } + opts.Host = snap + } + rep, err := preflight.Run(cmd.Context(), opts) + if err != nil { + return err + } + switch preflightFormat { + case "json": + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(rep); err != nil { + return err + } + default: + renderPreflight(os.Stdout, rep, useColor()) + } + if rep.Errors() > 0 { + os.Exit(1) + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(preflightCmd) + preflightCmd.Flags().StringVarP(&preflightFormat, "format", "f", "term", "Output format (term, json)") + preflightCmd.Flags().BoolVar(&preflightOffline, "offline", false, "Skip registry lookups") + preflightCmd.Flags().StringVar(&preflightHost, "host", "", "Predict for the machine in this snapshot file instead of this one (see `snapshot`)") +} + +type palette struct{ red, yellow, green, dim, bold, reset string } + +func useColor() bool { + if os.Getenv("NO_COLOR") != "" { + return false + } + st, err := os.Stdout.Stat() + return err == nil && st.Mode()&os.ModeCharDevice != 0 +} + +func renderPreflight(w io.Writer, rep *preflight.Report, color bool) { + c := palette{} + if color { + c = palette{"\033[31m", "\033[33m", "\033[32m", "\033[2m", "\033[1m", "\033[0m"} + } + icon := func(s fact.Severity) string { + switch s { + case fact.Error: + return c.red + "✗" + c.reset + case fact.Warning: + return c.yellow + "!" + c.reset + } + return c.dim + "i" + c.reset + } + + h := rep.Host + fmt.Fprintf(w, "\n%sContainer Doctor · preflight%s %s%s%s\n", c.bold, c.reset, c.dim, rep.Dir, c.reset) + if rep.HostName != "" { + fmt.Fprintf(w, "%sPredicting for snapshot of %s%s\n", c.yellow, rep.HostName, c.reset) + } + var hostBits []string + if h.OperatingSystem != "" { + hostBits = append(hostBits, h.OperatingSystem, h.Platform, humanSize(h.MemTotal)+" RAM for containers") + } + if h.ComposeVersion != "" { + hostBits = append(hostBits, "Compose "+h.ComposeVersion) + } + if h.BuildxInstalled { + hostBits = append(hostBits, "buildx "+h.BuildxVersion) + } + if len(hostBits) > 0 { + fmt.Fprintf(w, "%sHost: %s%s\n", c.dim, strings.Join(hostBits, " · "), c.reset) + } + + if len(rep.Roots) > 0 { + fmt.Fprintf(w, "\n%sROOT CAUSES%s\n", c.bold, c.reset) + for _, rc := range rep.Roots { + fmt.Fprintf(w, " %s %s\n", icon(rc.Severity), rc.Summary) + if rc.Detail != "" { + fmt.Fprintf(w, " %s%s%s\n", c.dim, rc.Detail, c.reset) + } + if rc.Fix != "" { + fmt.Fprintf(w, " fix: %s\n", rc.Fix) + } + if len(rc.Blocks) > 0 { + fmt.Fprintf(w, " %sblocks %d check(s): %s%s\n", c.dim, len(rc.Blocks), strings.Join(rc.Blocks, ", "), c.reset) + } + } + } + + var errs, warns int + for _, f := range rep.Findings { + switch f.Severity { + case fact.Error: + errs++ + case fact.Warning: + warns++ + } + } + if len(rep.Findings) > 0 { + fmt.Fprintf(w, "\n%sPREDICTED PROBLEMS%s %d error(s), %d warning(s)\n", c.bold, c.reset, errs, warns) + for _, f := range rep.Findings { + where := "" + if f.Service != "" { + where = "[" + f.Service + "] " + } + fmt.Fprintf(w, " %s %s%s", icon(f.Severity), where, f.Title) + if f.Location != "" { + fmt.Fprintf(w, " %s%s%s", c.dim, f.Location, c.reset) + } + fmt.Fprintln(w) + for _, e := range f.Evidence { + fmt.Fprintf(w, " %s%s%s\n", c.dim, e, c.reset) + } + if f.Predicts != "" { + fmt.Fprintf(w, " %swould fail with:%s %q\n", c.dim, c.reset, f.Predicts) + } + if f.Fix != "" { + fmt.Fprintf(w, " fix: %s\n", f.Fix) + } + } + } + + if len(rep.Passed) > 0 { + fmt.Fprintf(w, "\n%s✓ passed:%s %s%s%s\n", c.green, c.reset, c.dim, strings.Join(rep.Passed, ", "), c.reset) + } + + total := rep.Errors() + fmt.Fprintln(w) + if total > 0 { + fmt.Fprintf(w, "%s%s✗ Will fail:%s %d blocking problem(s) predicted %s(%s)%s\n\n", c.bold, c.red, c.reset, total, c.dim, rep.Duration.Round(1e6), c.reset) + } else if warns > 0 { + fmt.Fprintf(w, "%s%s! Should run%s, with %d warning(s) %s(%s)%s\n\n", c.bold, c.yellow, c.reset, warns, c.dim, rep.Duration.Round(1e6), c.reset) + } else { + fmt.Fprintf(w, "%s%s✓ Ready to run%s %s(%s)%s\n\n", c.bold, c.green, c.reset, c.dim, rep.Duration.Round(1e6), c.reset) + } +} + +func humanSize(n int64) string { + const unit = 1024 + if n < unit { + return fmt.Sprintf("%d B", n) + } + div, exp := int64(unit), 0 + for m := n / unit; m >= unit; m /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp]) +} diff --git a/cmd/root.go b/cmd/root.go index 6add7cd..289286d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,5 @@ /* Copyright © 2026 NAME HERE - */ package cmd @@ -10,18 +9,14 @@ import ( "github.com/spf13/cobra" ) - - // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "container-doctor", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, + Short: "Predict and diagnose container failures before they happen", + Long: `container-doctor checks whether a container project will build and run +on a given machine. ` + "`preflight`" + ` joins what the project needs (from its +Compose file and Dockerfiles) with what the host offers and what registries +publish, and reports each predicted failure once, at its root cause.`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -45,7 +40,4 @@ func init() { // Cobra also supports local flags, which will only run // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } - - diff --git a/cmd/snapshot.go b/cmd/snapshot.go new file mode 100644 index 0000000..5f206ba --- /dev/null +++ b/cmd/snapshot.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "encoding/json" + "os" + + "github.com/spf13/cobra" + + "github.com/raphgm/container-doctor/internal/executor" + "github.com/raphgm/container-doctor/pkg/preflight/host" +) + +var snapshotOut string + +var snapshotCmd = &cobra.Command{ + Use: "snapshot", + Short: "Save this machine's container capabilities to a file", + Long: `Snapshot records what this machine offers containers: Docker daemon +platform and memory, emulation, tooling versions, kernel settings, local +images and listening ports. Share the file, then run + + container-doctor preflight --host snapshot.json + +on any machine to predict whether a project will work on this one.`, + RunE: func(cmd *cobra.Command, args []string) error { + snap := host.Capture(cmd.Context(), executor.New()) + out := os.Stdout + if snapshotOut != "" && snapshotOut != "-" { + f, err := os.Create(snapshotOut) + if err != nil { + return err + } + defer f.Close() + out = f + } + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + return enc.Encode(snap) + }, +} + +func init() { + rootCmd.AddCommand(snapshotCmd) + snapshotCmd.Flags().StringVarP(&snapshotOut, "output", "o", "", "Write to this file instead of stdout") +} diff --git a/docs/validation/2026-09-23-colima-m-series.md b/docs/validation/2026-09-23-colima-m-series.md new file mode 100644 index 0000000..15d1c68 --- /dev/null +++ b/docs/validation/2026-09-23-colima-m-series.md @@ -0,0 +1,50 @@ +# Validation run: Apple silicon Mac, Colima + +Date: 2026-09-23 +Host: MacBook (arm64, 8 GiB), macOS, Colima 0.10 (vz driver, no Rosetta), VM Ubuntu 24.04.4, 2 CPUs, 1.9 GiB memory. +Tooling: Docker CLI 29.8.1, docker-compose 5.5.1 (standalone), no buildx plugin. +Project: `examples/broken-shop`. + +Each preflight prediction was checked by running the real Docker command. Where the real error was +worded differently from the predicted text, the rule was corrected and the corrected text is shown. + +## Predictions vs. reality + +| # | Rule | Prediction | Real outcome | Verdict | +|---|------|-----------|--------------|---------| +| 1 | `registry.credentials` | `error getting credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH` | identical | ✅ exact | +| 2 | `image.exists` | `redis:7.9.99-alpine` missing | `failed to resolve reference "docker.io/library/redis:7.9.99-alpine": … not found` | ✅ (wording corrected) | +| 3 | `build.buildkit` | `the --mount option requires BuildKit` | identical | ✅ exact | +| 4 | `build.context` | `package-lock.json` missing | `COPY failed: file not found in build context or excluded by .dockerignore: stat package-lock.json: file does not exist` | ✅ (legacy-builder wording added) | +| 5 | `compose.env` | `required variable DB_PASSWORD is missing a value` | identical | ✅ exact | +| 6 | `ports.conflict` | two services on 8080 | `Bind for 0.0.0.0:18080 failed: port is already allocated` | ✅ exact | +| 7 | `ports.conflict` | 2 replicas on one host port | `Bind for 0.0.0.0:18081 failed: port is already allocated` | ✅ exact | +| 8 | `mounts.bind` | missing `nginx.conf` becomes a directory | `not a directory: Are you trying to mount a directory onto a file (or vice-versa)?` | ✅ exact | +| 9 | `image.platform` | first version: mssql "no emulation → exec format error" | amd64 runs via QEMU (`uname -m` → `x86_64`) | ❌ false positive, fixed: kernel binfmt is now read inside the VM | +| 10 | `image.platform` + `resources.memory` | mssql needs ≥2000 MB, VM has 1.9 GiB | crashed earlier under QEMU: `Invalid mapping of address … in reserved address space` | ⚠️ memory finding true but masked; new QEMU-crash knowledge added, now predicted exactly | +| 11 | `kernel.sysctl` | first version: "passed" without data | VM `vm.max_map_count` = 1048576 | ⚠️ silent pass fixed: value now read from VM; unknown → warning | + +## Root-cause diagnosis (before Colima was started) + +The daemon failure was first reported generically. On this machine the actual cause was a Docker +context (`desktop-linux`) still pointing at the socket of an uninstalled Docker Desktop, with Colima +installed but stopped. Preflight now reports: + +``` +✗ Docker context "desktop-linux" still points to Docker Desktop, which is not installed + /Users/…/.docker/run/docker.sock is left over from it; nothing is listening there + fix: Start Colima: `colima start`, then `docker context use colima` + blocks 3 check(s): image.platform, resources.memory, resources.gpu +``` + +Following the fix started a working daemon. The first `colima start` failed because the Mac's disk had +118 MiB free; that is a host condition preflight does not yet diagnose. + +## Observations for the evaluation + +- Docker stops at the first failure: pulling surfaced only the credential error, not the missing tag; + the build surfaced only the missing `COPY` source, not `RUN --mount`. Preflight reported all of them + in one 4-second run. +- Of 11 predictions, 8 identified the real problem (6 word-for-word, 2 with different wording), 1 was a false + positive, and 2 were imprecise. All three were caused by facts inside the VM that the first version + could not see; reading the VM kernel fixed them. diff --git a/examples/broken-shop/compose.yaml b/examples/broken-shop/compose.yaml new file mode 100644 index 0000000..f683fc5 --- /dev/null +++ b/examples/broken-shop/compose.yaml @@ -0,0 +1,35 @@ +# A realistic project with problems that only show up on some machines. +services: + web: + build: + context: ./web + ports: + - "8080:80" + deploy: + replicas: 2 + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + environment: + API_URL: ${API_URL} + develop: + watch: + - path: ./web + action: rebuild + + api: + image: node:20-alpine + ports: + - "8080:3000" + environment: + DB_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env} + + db: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + ACCEPT_EULA: "Y" + + search: + image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0 + + cache: + image: redis:7.9.99-alpine diff --git a/examples/broken-shop/web/.dockerignore b/examples/broken-shop/web/.dockerignore new file mode 100644 index 0000000..6a0d87e --- /dev/null +++ b/examples/broken-shop/web/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.env* diff --git a/examples/broken-shop/web/.env.production b/examples/broken-shop/web/.env.production new file mode 100644 index 0000000..e69de29 diff --git a/examples/broken-shop/web/Dockerfile b/examples/broken-shop/web/Dockerfile new file mode 100644 index 0000000..8ed0493 --- /dev/null +++ b/examples/broken-shop/web/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci +COPY src ./src +RUN npm run build + +FROM nginx:1.27-alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY .env.production /etc/app.env diff --git a/examples/broken-shop/web/package.json b/examples/broken-shop/web/package.json new file mode 100644 index 0000000..200614f --- /dev/null +++ b/examples/broken-shop/web/package.json @@ -0,0 +1 @@ +{"name":"web"} diff --git a/examples/broken-shop/web/src/index.js b/examples/broken-shop/web/src/index.js new file mode 100644 index 0000000..e14c4f2 --- /dev/null +++ b/examples/broken-shop/web/src/index.js @@ -0,0 +1 @@ +console.log(1) diff --git a/examples/hosts/macbook-m2-desktop.json b/examples/hosts/macbook-m2-desktop.json new file mode 100644 index 0000000..444fac6 --- /dev/null +++ b/examples/hosts/macbook-m2-desktop.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "capturedAt": "2026-09-23T00:00:00Z", + "hostname": "example: MacBook Air M2, Docker Desktop defaults", + "profile": { + "ClientOS": "darwin", + "ClientArch": "arm64", + "ClientMem": 17179869184, + "Platform": "linux/arm64", + "OperatingSystem": "Docker Desktop", + "ServerVersion": "27.2.0", + "KernelVersion": "6.10.4-linuxkit", + "MemTotal": 4108189696, + "NCPU": 8, + "Runtimes": ["io.containerd.runc.v2", "runc"], + "CgroupVersion": "2", + "DockerRootDir": "/var/lib/docker", + "Endpoint": "unix:///Users/me/.docker/run/docker.sock", + "DiskFree": 48318382080, + "ComposeVersion": "2.20.2-desktop.1", + "BuildxInstalled": true, + "BuildxVersion": "0.12.1-desktop.4", + "Emulated": ["linux/amd64"], + "Sysctls": {"vm.max_map_count": 262144}, + "Local": true + }, + "listeningPorts": ["5000/tcp", "7000/tcp"] +} diff --git a/examples/hosts/ubuntu-legacy-server.json b/examples/hosts/ubuntu-legacy-server.json new file mode 100644 index 0000000..d9aaf00 --- /dev/null +++ b/examples/hosts/ubuntu-legacy-server.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "capturedAt": "2026-09-23T00:00:00Z", + "hostname": "example: Ubuntu 20.04 server, distro Docker packages", + "profile": { + "ClientOS": "linux", + "ClientArch": "amd64", + "ClientMem": 8335405056, + "Platform": "linux/amd64", + "OperatingSystem": "Ubuntu 20.04.6 LTS", + "ServerVersion": "20.10.21", + "KernelVersion": "5.4.0-196-generic", + "MemTotal": 8335405056, + "NCPU": 4, + "Runtimes": ["io.containerd.runc.v2", "runc"], + "CgroupVersion": "1", + "DockerRootDir": "/var/lib/docker", + "Endpoint": "unix:///var/run/docker.sock", + "DiskFree": 3221225472, + "DiskExact": true, + "ComposeVersion": "1.29.2", + "BuildxInstalled": false, + "Sysctls": {"vm.max_map_count": 65530, "net.ipv4.ip_unprivileged_port_start": 1024}, + "Local": true + }, + "listeningPorts": ["22/tcp", "8080/tcp"] +} diff --git a/go.mod b/go.mod index a682ca4..29b9c12 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,46 @@ module github.com/raphgm/container-doctor go 1.26.5 require ( + github.com/compose-spec/compose-go/v2 v2.15.0 + github.com/google/go-containerregistry v0.22.1 + github.com/moby/buildkit v0.33.0 + github.com/moby/patternmatcher v0.6.1 github.com/shirou/gopsutil/v3 v3.24.5 + github.com/sirupsen/logrus v1.10.1 github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/containerd/typeurl/v2 v2.3.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v29.7.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.8 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.19.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/sys v0.20.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect ) diff --git a/go.sum b/go.sum index b4e3de7..b0c736f 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,100 @@ +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/compose-spec/compose-go/v2 v2.15.0 h1:tdQw+eMyT+P6ZIb09JfcIVvbMmIa+PjST7cWezVLf00= +github.com/compose-spec/compose-go/v2 v2.15.0/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= +github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= +github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY= +github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= +github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= +github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/moby/buildkit v0.33.0 h1:zBbt1FiMcTB/oFg1iCNcKa83k5Rn8MGcVjXFIcfYhuQ= +github.com/moby/buildkit v0.33.0/go.mod h1:uNKSZnfMk1aSa18JiCR5BT68M/3jIDGo6XuAByUK3ek= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= +github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= +go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/pkg/preflight/fact/fact.go b/pkg/preflight/fact/fact.go new file mode 100644 index 0000000..089da3d --- /dev/null +++ b/pkg/preflight/fact/fact.go @@ -0,0 +1,111 @@ +// Package fact defines the facts preflight depends on and the causal graph +// between them. A fact "fails" when it cannot be determined; every rule that +// needs a failed fact is reported as blocked by that fact's root cause instead +// of producing its own, misleading, failure. +package fact + +import "sort" + +type ID string + +const ( + DockerCLI ID = "docker.cli" + Daemon ID = "docker.daemon" + LocalDaemon ID = "docker.local" + Compose ID = "docker.compose" + Buildx ID = "docker.buildx" + Registry ID = "registry.network" + ComposeFile ID = "project.compose" + Dockerfile ID = "project.dockerfile" +) + +// parents encodes the causal edges: a fact cannot be known unless its parent +// is known. LocalDaemon ("the daemon endpoint is on this machine") comes +// from the CLI context, so host-side checks still run when the daemon is +// down. +var parents = map[ID]ID{ + Daemon: DockerCLI, + LocalDaemon: DockerCLI, + Compose: DockerCLI, + Buildx: DockerCLI, +} + +// Parent returns the direct causal parent of id, if any. +func Parent(id ID) (ID, bool) { + p, ok := parents[id] + return p, ok +} + +type Severity string + +const ( + Error Severity = "ERROR" + Warning Severity = "WARN" + Info Severity = "INFO" +) + +// Failure explains why a fact could not be determined. +type Failure struct { + Fact ID + Severity Severity + Summary string + Detail string + Fix string +} + +// Set records which facts failed. +type Set struct { + failed map[ID]Failure +} + +func NewSet() *Set { + return &Set{failed: map[ID]Failure{}} +} + +func (s *Set) Fail(f Failure) { + if _, exists := s.failed[f.Fact]; !exists { + s.failed[f.Fact] = f + } +} + +// Failed reports whether id is unknown, either directly or because an +// ancestor failed. +func (s *Set) Failed(id ID) bool { + _, ok := s.Root(id) + return ok +} + +// Root returns the top-most failed fact on id's ancestor chain. Fixing the +// root is what unblocks id. +func (s *Set) Root(id ID) (Failure, bool) { + var root Failure + found := false + for cur, ok := id, true; ok; cur, ok = Parent(cur) { + if f, failed := s.failed[cur]; failed { + root, found = f, true + } + } + return root, found +} + +// All returns every recorded failure. +func (s *Set) All() []Failure { + var out []Failure + for _, f := range s.failed { + out = append(out, f) + } + sort.Slice(out, func(i, j int) bool { return out[i].Fact < out[j].Fact }) + return out +} + +// Roots returns every failed fact whose ancestors are all known. +func (s *Set) Roots() []Failure { + var out []Failure + for id, f := range s.failed { + if r, _ := s.Root(id); r.Fact == id { + out = append(out, f) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].Fact < out[j].Fact }) + return out +} diff --git a/pkg/preflight/fact/fact_test.go b/pkg/preflight/fact/fact_test.go new file mode 100644 index 0000000..1464505 --- /dev/null +++ b/pkg/preflight/fact/fact_test.go @@ -0,0 +1,39 @@ +package fact + +import "testing" + +func TestRootWalksToTopMostFailure(t *testing.T) { + s := NewSet() + s.Fail(Failure{Fact: DockerCLI, Summary: "no docker"}) + s.Fail(Failure{Fact: Daemon, Summary: "daemon down"}) + + r, ok := s.Root(Daemon) + if !ok || r.Fact != DockerCLI { + t.Fatalf("root of Daemon = %v, %v; want DockerCLI", r.Fact, ok) + } + roots := s.Roots() + if len(roots) != 1 || roots[0].Fact != DockerCLI { + t.Fatalf("roots = %+v; want only DockerCLI", roots) + } +} + +func TestDaemonDownLeavesHostFactsKnown(t *testing.T) { + s := NewSet() + s.Fail(Failure{Fact: Daemon, Summary: "daemon down"}) + if s.Failed(LocalDaemon) || s.Failed(Compose) { + t.Fatal("endpoint locality and Compose do not depend on the daemon") + } +} + +func TestCLIFailureBlocksEverythingDockerSide(t *testing.T) { + s := NewSet() + s.Fail(Failure{Fact: DockerCLI}) + for _, id := range []ID{Daemon, LocalDaemon, Compose, Buildx} { + if r, _ := s.Root(id); r.Fact != DockerCLI { + t.Errorf("root of %s = %s; want %s", id, r.Fact, DockerCLI) + } + } + if s.Failed(Registry) { + t.Error("registry reachability does not depend on the docker CLI") + } +} diff --git a/pkg/preflight/host/diagnose.go b/pkg/preflight/host/diagnose.go new file mode 100644 index 0000000..4edd4d7 --- /dev/null +++ b/pkg/preflight/host/diagnose.go @@ -0,0 +1,157 @@ +package host + +import ( + "fmt" + "os" + osexec "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +// engine is a local Docker engine this machine could use. +type engine struct { + Name string + Socket string // socket path when the engine is running + Start string // command or action that starts it + Context string // docker context it registers +} + +// daemonClues is what diagnoseDaemon reasons over. It is gathered by +// gatherClues and kept separate so the reasoning can be tested. +type daemonClues struct { + Context string + Endpoint string + Err string + Installed []engine + Running []engine +} + +// diagnoseDaemon turns "cannot connect" into the specific reason: the +// context points at an engine that is gone, an installed engine is stopped, +// another engine is running under a different context, or permissions. +func diagnoseDaemon(c daemonClues) fact.Failure { + f := fact.Failure{ + Fact: fact.Daemon, + Severity: fact.Error, + Summary: "Docker daemon is not reachable", + Detail: c.Err, + } + socket := strings.TrimPrefix(c.Endpoint, "unix://") + ctxName := c.Context + if ctxName == "" { + ctxName = "default" + } + + switch { + case strings.Contains(c.Err, "permission denied"): + f.Summary = "No permission to use the Docker socket " + socket + f.Fix = "Add yourself to the docker group (`sudo usermod -aG docker $USER`, then log in again), or use rootless Docker." + return f + + case len(c.Running) > 0 && !strings.EqualFold(c.Running[0].Socket, socket): + r := c.Running[0] + f.Summary = fmt.Sprintf("%s is running, but the Docker context %q points elsewhere (%s)", r.Name, ctxName, socket) + f.Fix = "`docker context use " + r.Context + "`" + return f + + case strings.HasPrefix(c.Endpoint, "unix://"): + owner := socketOwner(socket) + ownerInstalled := false + for _, e := range c.Installed { + if e.Name == owner { + ownerInstalled = true + } + } + switch { + case owner != "" && !ownerInstalled: + f.Summary = fmt.Sprintf("Docker context %q still points to %s, which is not installed", ctxName, owner) + f.Detail = socket + " is left over from it; nothing is listening there" + case !fileExists(socket): + f.Summary = fmt.Sprintf("Docker context %q points to %s, which does not exist", ctxName, socket) + case owner != "": + f.Summary = owner + " is installed but not running" + } + } + + switch len(c.Installed) { + case 0: + f.Fix = "Install a Docker engine (Docker Desktop, OrbStack or Colima) and start it." + default: + e := c.Installed[0] + f.Fix = fmt.Sprintf("Start %s: `%s`", e.Name, e.Start) + if e.Context != "" && e.Context != ctxName { + f.Fix += fmt.Sprintf(", then `docker context use %s`", e.Context) + } + if len(c.Installed) > 1 { + var others []string + for _, o := range c.Installed[1:] { + others = append(others, o.Name) + } + f.Fix += " (also installed: " + strings.Join(others, ", ") + ")" + } + } + return f +} + +func socketOwner(socket string) string { + switch { + case strings.Contains(socket, "/.docker/run/"), strings.Contains(socket, "docker-desktop"): + return "Docker Desktop" + case strings.Contains(socket, "/.colima/"): + return "Colima" + case strings.Contains(socket, "/.orbstack/"): + return "OrbStack" + case strings.Contains(socket, "podman"): + return "Podman" + } + return "" +} + +// gatherClues looks for local engines: installed apps and binaries, and the +// sockets of those that are running. +func gatherClues(ctxName, endpoint, errText string) daemonClues { + c := daemonClues{Context: ctxName, Endpoint: endpoint, Err: errText} + home, _ := os.UserHomeDir() + + candidates := []struct { + engine + installed func() bool + }{ + {engine{"Colima", filepath.Join(home, ".colima/default/docker.sock"), "colima start", "colima"}, func() bool { return onPath("colima") }}, + {engine{"OrbStack", filepath.Join(home, ".orbstack/run/docker.sock"), "open -a OrbStack", "orbstack"}, func() bool { + return fileExists("/Applications/OrbStack.app") || onPath("orb") + }}, + {engine{"Docker Desktop", filepath.Join(home, ".docker/run/docker.sock"), "open -a Docker", "desktop-linux"}, func() bool { + return fileExists("/Applications/Docker.app") + }}, + } + if runtime.GOOS == "linux" { + candidates = append(candidates, struct { + engine + installed func() bool + }{engine{"Docker Engine", "/var/run/docker.sock", "sudo systemctl start docker", "default"}, func() bool { return onPath("dockerd") }}) + } + + for _, cand := range candidates { + if fileExists(cand.Socket) { + c.Running = append(c.Running, cand.engine) + } + if cand.installed() { + c.Installed = append(c.Installed, cand.engine) + } + } + return c +} + +func onPath(bin string) bool { + _, err := osexec.LookPath(bin) + return err == nil +} + +func fileExists(p string) bool { + _, err := os.Stat(p) + return err == nil +} diff --git a/pkg/preflight/host/diagnose_test.go b/pkg/preflight/host/diagnose_test.go new file mode 100644 index 0000000..a5b85d3 --- /dev/null +++ b/pkg/preflight/host/diagnose_test.go @@ -0,0 +1,63 @@ +package host + +import ( + "strings" + "testing" +) + +var colima = engine{"Colima", "/h/.colima/default/docker.sock", "colima start", "colima"} + +func TestDiagnoseStaleDesktopContext(t *testing.T) { + f := diagnoseDaemon(daemonClues{ + Context: "desktop-linux", + Endpoint: "unix:///nonexistent/.docker/run/docker.sock", + Err: "Cannot connect to the Docker daemon", + Installed: []engine{colima}, + }) + if f.Summary != `Docker context "desktop-linux" still points to Docker Desktop, which is not installed` { + t.Errorf("summary = %q", f.Summary) + } + if f.Fix != "Start Colima: `colima start`, then `docker context use colima`" { + t.Errorf("fix = %q", f.Fix) + } +} + +func TestDiagnoseRunningEngineUnderOtherContext(t *testing.T) { + f := diagnoseDaemon(daemonClues{ + Context: "desktop-linux", + Endpoint: "unix:///nonexistent/.docker/run/docker.sock", + Installed: []engine{colima}, + Running: []engine{colima}, + }) + if !strings.Contains(f.Summary, "Colima is running") || f.Fix != "`docker context use colima`" { + t.Errorf("got %q / %q", f.Summary, f.Fix) + } +} + +func TestDiagnosePermission(t *testing.T) { + f := diagnoseDaemon(daemonClues{Endpoint: "unix:///var/run/docker.sock", Err: "dial unix /var/run/docker.sock: connect: permission denied"}) + if !strings.Contains(f.Fix, "usermod -aG docker") { + t.Errorf("fix = %q", f.Fix) + } +} + +func TestDiagnoseNothingInstalled(t *testing.T) { + f := diagnoseDaemon(daemonClues{Endpoint: "unix:///nonexistent.sock"}) + if !strings.Contains(f.Fix, "Install a Docker engine") { + t.Errorf("fix = %q", f.Fix) + } +} + +func TestDiagnoseInstalledButStopped(t *testing.T) { + f := diagnoseDaemon(daemonClues{ + Context: "colima", + Endpoint: "unix:///nonexistent/.colima/default/docker.sock", + Installed: []engine{colima}, + }) + if f.Summary != "Colima is installed but not running" && !strings.Contains(f.Summary, "does not exist") { + t.Errorf("summary = %q", f.Summary) + } + if f.Fix != "Start Colima: `colima start`" { + t.Errorf("fix = %q", f.Fix) + } +} diff --git a/pkg/preflight/host/host.go b/pkg/preflight/host/host.go new file mode 100644 index 0000000..3e78935 --- /dev/null +++ b/pkg/preflight/host/host.go @@ -0,0 +1,553 @@ +// Package host builds a capability profile of the machine that will run the +// project: the Docker daemon's platform and resources (which on macOS and +// Windows are the VM's, not the laptop's), installed tooling, emulation, and +// live port usage. +package host + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + osexec "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" + + "github.com/raphgm/container-doctor/internal/executor" + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +type Profile struct { + // ClientOS/ClientArch describe the machine preflight runs on. + ClientOS string + ClientArch string + ClientMem int64 + + // Platform is the daemon's native platform, e.g. linux/arm64. + Platform string + OperatingSystem string + ServerVersion string + KernelVersion string + MemTotal int64 + NCPU int + Runtimes []string + Rootless bool + CgroupVersion string + DockerRootDir string + Endpoint string + + // DiskFree is the free space where images are stored. DiskExact is + // false when only an upper bound is known (Docker Desktop's VM disk + // cannot be measured from the host). + DiskFree int64 + DiskExact bool + + ComposeVersion string + BuildxInstalled bool + BuildxVersion string + BuildKitDisabled bool + + // Emulated lists foreign platforms the host can run or build. + // EmulationKnown is false when the daemon's kernel could not be + // inspected, so an empty Emulated list proves nothing. + Emulated []string + EmulationKnown bool + + // Emulators maps an emulated platform to the mechanism: "qemu" or + // "rosetta". Some software runs under Rosetta but crashes under QEMU. + Emulators map[string]string + + // Sysctls holds kernel settings read on a local Linux daemon host. + Sysctls map[string]int64 + + // LocalImages holds canonical references already present in the + // daemon; Compose does not pull these. + LocalImages map[string]bool + + // PublishedPorts maps "port/proto" to containers already publishing it. + PublishedPorts map[string][]Container + + // PortProbe and Lsof are nil on remote daemons. + PortProbe func(ip string, port int, proto string) bool `json:"-"` + Lsof func(port int, proto string) string `json:"-"` + + // Local is true when the daemon endpoint is on this machine. + Local bool + + // MissingCredentialHelper names a credsStore/credHelpers program that + // ~/.docker/config.json configures but that is not installed. + MissingCredentialHelper string +} + +type Container struct { + Name string + ComposeProject string +} + +// IsDesktop reports whether the daemon runs inside Docker Desktop's VM. +func (p *Profile) IsDesktop() bool { + return strings.Contains(p.OperatingSystem, "Docker Desktop") +} + +// CanRun reports whether platform runs natively or under emulation. +func (p *Profile) CanRun(platform string) (native, emulated bool) { + if registry.Compatible(platform, p.Platform) { + return true, false + } + for _, e := range p.Emulated { + if registry.Compatible(platform, e) { + return false, true + } + } + return false, false +} + +// PortHolder reports what already holds a host port: a container, a local +// process, or "" when the port is free. +func (p *Profile) PortHolder(ip string, port int, proto string) (holder string, container *Container) { + if cs := p.PublishedPorts[fmt.Sprintf("%d/%s", port, proto)]; len(cs) > 0 { + return "container " + cs[0].Name, &cs[0] + } + if p.PortProbe != nil && p.PortProbe(ip, port, proto) { + if p.Lsof != nil { + if name := p.Lsof(port, proto); name != "" { + return "process " + name, nil + } + } + return "another process", nil + } + return "", nil +} + +// Probe inspects the host. Anything it cannot determine is recorded in +// facts with the reason, so dependent rules report the true root cause. +func Probe(ctx context.Context, run executor.Runner, facts *fact.Set) *Profile { + p := &Profile{ + ClientOS: runtime.GOOS, + ClientArch: runtime.GOARCH, + Sysctls: map[string]int64{}, + PublishedPorts: map[string][]Container{}, + LocalImages: map[string]bool{}, + } + if vm, err := mem.VirtualMemoryWithContext(ctx); err == nil { + p.ClientMem = int64(vm.Total) + } + p.BuildKitDisabled = os.Getenv("DOCKER_BUILDKIT") == "0" + + if _, err := run.Run(ctx, "docker", "--version"); err != nil { + facts.Fail(fact.Failure{ + Fact: fact.DockerCLI, + Severity: fact.Error, + Summary: "Docker CLI not found", + Detail: errText(err), + Fix: "Install Docker Desktop, OrbStack, Colima or Docker Engine, and make sure `docker` is on PATH.", + }) + return p + } + + p.MissingCredentialHelper = missingCredentialHelper() + p.probeEndpoint(ctx, run, facts) + p.probeDaemon(ctx, run, facts) + p.probeCompose(ctx, run, facts) + p.probeBuildx(ctx, run) + return p +} + +func (p *Profile) probeDaemon(ctx context.Context, run executor.Runner, facts *fact.Set) { + out, err := run.Run(ctx, "docker", "info", "--format", "{{json .}}") + var info dockerInfo + if err == nil { + err = json.Unmarshal([]byte(out), &info) + } + if err == nil && len(info.ServerErrors) > 0 { + err = errors.New(strings.Join(info.ServerErrors, "; ")) + } + if err == nil && info.ServerVersion == "" { + err = errors.New("docker info returned no server details") + } + if err != nil { + ctxName, _ := run.Run(ctx, "docker", "context", "show") + facts.Fail(diagnoseDaemon(gatherClues(ctxName, p.Endpoint, errText(err)))) + return + } + + p.Platform = registry.Normalize(info.OSType + "/" + info.Architecture) + p.OperatingSystem = info.OperatingSystem + p.ServerVersion = info.ServerVersion + p.KernelVersion = info.KernelVersion + p.MemTotal = info.MemTotal + p.NCPU = info.NCPU + p.CgroupVersion = info.CgroupVersion + p.DockerRootDir = info.DockerRootDir + for name := range info.Runtimes { + p.Runtimes = append(p.Runtimes, name) + } + for _, opt := range info.SecurityOptions { + if strings.Contains(opt, "name=rootless") { + p.Rootless = true + } + } + + if p.IsDesktop() { + // Docker Desktop's VM supports amd64 on Apple silicon (Rosetta or + // QEMU) and ships its own sysctl defaults. + if p.Platform == "linux/arm64" { + p.Emulated = append(p.Emulated, "linux/amd64") + } else if p.Platform == "linux/amd64" { + p.Emulated = append(p.Emulated, "linux/arm64", "linux/arm/v7") + } + p.Sysctls = map[string]int64{"vm.max_map_count": 262144} + p.EmulationKnown = true + p.DiskExact = false + } else if shell := p.kernelShell(); shell != nil { + p.readKernel(ctx, run, shell) + } + + if out, err := run.Run(ctx, "docker", "image", "ls", "--format", "{{.Repository}}:{{.Tag}}"); err == nil { + for _, ref := range strings.Split(out, "\n") { + if ref != "" && !strings.Contains(ref, "") { + p.LocalImages[registry.Canonical(ref)] = true + } + } + } + + if out, err := run.Run(ctx, "docker", "ps", "--format", "{{.Names}}\t{{.Label \"com.docker.compose.project\"}}\t{{.Ports}}"); err == nil { + p.PublishedPorts = parsePS(out) + } +} + +// probeEndpoint decides from the CLI context whether the daemon runs on +// this machine. Only then do local ports, paths and kernel settings apply. +func (p *Profile) probeEndpoint(ctx context.Context, run executor.Runner, facts *fact.Set) { + p.Endpoint = os.Getenv("DOCKER_HOST") + if p.Endpoint == "" { + p.Endpoint, _ = run.Run(ctx, "docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}") + } + p.Local = p.Endpoint == "" || strings.HasPrefix(p.Endpoint, "unix://") || strings.HasPrefix(p.Endpoint, "npipe://") + if !p.Local { + facts.Fail(fact.Failure{ + Fact: fact.LocalDaemon, + Severity: fact.Info, + Summary: "Docker daemon is remote (" + p.Endpoint + ")", + Detail: "Ports, bind-mount paths and kernel settings live on the remote host and cannot be checked from here.", + Fix: "Run container-doctor on the daemon host for full coverage.", + }) + return + } + + p.PortProbe = portInUse + p.Lsof = lsofHolder + if home, err := os.UserHomeDir(); err == nil { + if usage, err := disk.UsageWithContext(ctx, home); err == nil { + p.DiskFree = int64(usage.Free) + } + } +} + +// kernelShell returns the command prefix that runs a shell where the +// daemon's kernel lives: this machine for a local Linux daemon, or the VM +// for Colima. It returns nil when there is no way in. +func (p *Profile) kernelShell() []string { + if !p.Local { + return nil + } + sock := strings.TrimPrefix(p.Endpoint, "unix://") + if i := strings.Index(sock, "/.colima/"); i >= 0 { + profile := strings.SplitN(sock[i+len("/.colima/"):], "/", 2)[0] + if profile == "" || profile == "_lima" { + profile = "default" + } + return []string{"colima", "ssh", "-p", profile, "--"} + } + if runtime.GOOS == "linux" { + return []string{} + } + return nil +} + +// kernelScript prints the kernel facts preflight needs, one per line. +const kernelScript = `echo max_map_count=$(cat /proc/sys/vm/max_map_count 2>/dev/null) +echo unpriv_port_start=$(cat /proc/sys/net/ipv4/ip_unprivileged_port_start 2>/dev/null) +echo binfmt=$(ls /proc/sys/fs/binfmt_misc 2>/dev/null | tr '\n' ' ') +echo disk_free=$(df -B1 --output=avail "$0" 2>/dev/null | tail -1)` + +func (p *Profile) readKernel(ctx context.Context, run executor.Runner, shell []string) { + root := p.DockerRootDir + if root == "" { + root = "/var/lib/docker" + } + args := append(append([]string{}, shell...), "sh", "-c", kernelScript, root) + out, err := run.Run(ctx, args[0], args[1:]...) + if err != nil { + return + } + native := p.Platform + for _, line := range strings.Split(out, "\n") { + key, val, ok := strings.Cut(strings.TrimSpace(line), "=") + if !ok || val == "" { + continue + } + switch key { + case "max_map_count": + if v, err := strconv.ParseInt(val, 10, 64); err == nil { + p.Sysctls["vm.max_map_count"] = v + } + case "unpriv_port_start": + if v, err := strconv.ParseInt(val, 10, 64); err == nil { + p.Sysctls["net.ipv4.ip_unprivileged_port_start"] = v + } + case "binfmt": + p.EmulationKnown = true + for _, h := range strings.Fields(val) { + arch, ok := binfmtPlatforms[h] + if !ok || arch == native { + continue + } + if !containsStr(p.Emulated, arch) { + p.Emulated = append(p.Emulated, arch) + } + if p.Emulators == nil { + p.Emulators = map[string]string{} + } + // Rosetta wins when both are registered: the kernel + // prefers it on Apple virtualization. + if h == "rosetta" || p.Emulators[arch] == "" { + p.Emulators[arch] = strings.SplitN(h, "-", 2)[0] + } + } + case "disk_free": + if v, err := strconv.ParseInt(val, 10, 64); err == nil { + p.DiskFree, p.DiskExact = v, true + } + } + } +} + +// binfmtPlatforms maps binfmt_misc handler names (QEMU and Rosetta) to the +// platforms they run. +var binfmtPlatforms = map[string]string{ + "qemu-x86_64": "linux/amd64", + "rosetta": "linux/amd64", + "qemu-aarch64": "linux/arm64", + "qemu-arm": "linux/arm/v7", + "qemu-riscv64": "linux/riscv64", + "qemu-ppc64le": "linux/ppc64le", + "qemu-s390x": "linux/s390x", +} + +func (p *Profile) probeCompose(ctx context.Context, run executor.Runner, facts *fact.Set) { + if v, err := run.Run(ctx, "docker", "compose", "version", "--short"); err == nil { + p.ComposeVersion = strings.TrimPrefix(v, "v") + return + } + if v, err := run.Run(ctx, "docker-compose", "version", "--short"); err == nil { + p.ComposeVersion = strings.TrimPrefix(v, "v") + return + } + facts.Fail(fact.Failure{ + Fact: fact.Compose, + Severity: fact.Error, + Summary: "Docker Compose is not installed", + Fix: "Install the Compose v2 plugin (bundled with Docker Desktop; `docker-compose-plugin` package on Linux).", + }) +} + +func (p *Profile) probeBuildx(ctx context.Context, run executor.Runner) { + v, err := run.Run(ctx, "docker", "buildx", "version") + if err != nil { + return + } + p.BuildxInstalled = true + if f := strings.Fields(v); len(f) >= 2 { + p.BuildxVersion = strings.TrimPrefix(f[1], "v") + } + if out, err := run.Run(ctx, "docker", "buildx", "inspect"); err == nil { + for _, line := range strings.Split(out, "\n") { + if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Platforms:"); ok { + p.EmulationKnown = true + for _, pl := range strings.Split(rest, ",") { + pl = registry.Normalize(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(pl), "*"))) + if pl != "" && pl != p.Platform && !containsStr(p.Emulated, pl) { + p.Emulated = append(p.Emulated, pl) + } + } + } + } + } +} + +type dockerInfo struct { + ServerVersion string + OSType string + Architecture string + OperatingSystem string + KernelVersion string + MemTotal int64 + NCPU int + CgroupVersion string + DockerRootDir string + Runtimes map[string]json.RawMessage + SecurityOptions []string + ServerErrors []string +} + +// parsePS reads `docker ps` rows: name, compose project, ports. +func parsePS(out string) map[string][]Container { + res := map[string][]Container{} + for _, line := range strings.Split(out, "\n") { + cols := strings.Split(line, "\t") + if len(cols) < 3 { + continue + } + c := Container{Name: cols[0], ComposeProject: cols[1]} + for _, mapping := range strings.Split(cols[2], ", ") { + // 0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:8000-8001->80-81/tcp + left, right, ok := strings.Cut(mapping, "->") + if !ok { + continue + } + _, proto, _ := strings.Cut(right, "/") + hostPorts := left[strings.LastIndex(left, ":")+1:] + lo, hi, _ := strings.Cut(hostPorts, "-") + start, err := strconv.Atoi(lo) + if err != nil { + continue + } + end := start + if hi != "" { + if e, err := strconv.Atoi(hi); err == nil { + end = e + } + } + for port := start; port <= end; port++ { + key := fmt.Sprintf("%d/%s", port, proto) + if !containsContainer(res[key], c.Name) { + res[key] = append(res[key], c) + } + } + } + } + return res +} + +func portInUse(ip string, port int, proto string) bool { + if ip == "" { + ip = "0.0.0.0" + } + addr := net.JoinHostPort(ip, strconv.Itoa(port)) + if proto == "udp" { + c, err := net.ListenPacket("udp", addr) + if err != nil { + return true + } + c.Close() + return false + } + // A listener on 127.0.0.1 alone still blocks Docker's 0.0.0.0 bind on + // Linux, and on macOS a bind can succeed despite it, so dial as well. + if c, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), 300*time.Millisecond); err == nil { + c.Close() + return true + } + l, err := net.Listen("tcp", addr) + if err != nil { + return true + } + l.Close() + return false +} + +func lsofHolder(port int, proto string) string { + args := []string{"-nP", fmt.Sprintf("-i%s:%d", strings.ToUpper(proto), port), "-Fcp"} + if proto == "tcp" { + args = append(args, "-sTCP:LISTEN") + } + out, err := osexec.Command("lsof", args...).Output() + if err != nil { + return "" + } + var pid, cmd string + for _, line := range strings.Split(string(out), "\n") { + if strings.HasPrefix(line, "p") && pid == "" { + pid = line[1:] + } + if strings.HasPrefix(line, "c") && cmd == "" { + cmd = line[1:] + } + } + if cmd == "" { + return "" + } + return fmt.Sprintf("%s (pid %s)", cmd, pid) +} + +// missingCredentialHelper returns the first credential helper configured in +// the Docker CLI config that cannot be found on PATH. +func missingCredentialHelper() string { + dir := os.Getenv("DOCKER_CONFIG") + if dir == "" { + home, err := os.UserHomeDir() + if err != nil { + return "" + } + dir = filepath.Join(home, ".docker") + } + b, err := os.ReadFile(filepath.Join(dir, "config.json")) + if err != nil { + return "" + } + var cfg struct { + CredsStore string `json:"credsStore"` + CredHelpers map[string]string `json:"credHelpers"` + } + if json.Unmarshal(b, &cfg) != nil { + return "" + } + helpers := []string{cfg.CredsStore} + for _, h := range cfg.CredHelpers { + helpers = append(helpers, h) + } + for _, h := range helpers { + if h == "" { + continue + } + if _, err := osexec.LookPath("docker-credential-" + h); err != nil { + return "docker-credential-" + h + } + } + return "" +} + +func errText(err error) string { + if errors.Is(err, osexec.ErrNotFound) { + return "`docker` executable not found on PATH" + } + return strings.TrimPrefix(strings.TrimSpace(err.Error()), "exit status 1: ") +} + +func containsStr(xs []string, s string) bool { + for _, x := range xs { + if x == s { + return true + } + } + return false +} + +func containsContainer(cs []Container, name string) bool { + for _, c := range cs { + if c.Name == name { + return true + } + } + return false +} diff --git a/pkg/preflight/host/host_test.go b/pkg/preflight/host/host_test.go new file mode 100644 index 0000000..9c5650c --- /dev/null +++ b/pkg/preflight/host/host_test.go @@ -0,0 +1,127 @@ +package host + +import ( + "context" + "errors" + "fmt" + "os/exec" + "strings" + "testing" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +type fakeRunner map[string]string + +func (f fakeRunner) Run(_ context.Context, name string, args ...string) (string, error) { + key := strings.TrimSpace(name + " " + strings.Join(args, " ")) + for prefix, out := range f { + if strings.HasPrefix(key, prefix) { + if strings.HasPrefix(out, "ERR:") { + return "", errors.New(strings.TrimPrefix(out, "ERR:")) + } + return out, nil + } + } + return "", fmt.Errorf("unexpected command %q", key) +} + +type missingDocker struct{} + +func (missingDocker) Run(context.Context, string, ...string) (string, error) { + return "", fmt.Errorf("%w", exec.ErrNotFound) +} + +func TestNoDockerCLIIsTheOnlyRootCause(t *testing.T) { + facts := fact.NewSet() + Probe(context.Background(), missingDocker{}, facts) + roots := facts.Roots() + if len(roots) != 1 || roots[0].Fact != fact.DockerCLI { + t.Fatalf("roots = %+v", roots) + } + for _, id := range []fact.ID{fact.Daemon, fact.Compose, fact.Buildx, fact.LocalDaemon} { + if !facts.Failed(id) { + t.Errorf("%s should be unknown without the CLI", id) + } + } +} + +func TestDaemonDown(t *testing.T) { + facts := fact.NewSet() + p := Probe(context.Background(), fakeRunner{ + "docker --version": "Docker version 27.0.3", + "docker info": "ERR:Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?", + "docker compose": "2.29.1", + "docker buildx version": "github.com/docker/buildx v0.16.1 abc", + "docker buildx inspect": "Platforms: linux/arm64", + "docker ps": "ERR:cannot connect", + }, facts) + r, ok := facts.Root(fact.Daemon) + if !ok || r.Fact != fact.Daemon || !strings.Contains(r.Detail, "Cannot connect") { + t.Fatalf("root = %+v", r) + } + if facts.Failed(fact.LocalDaemon) || p.PortProbe == nil { + t.Error("a local endpoint stays checkable while the daemon is down") + } + if facts.Failed(fact.Compose) || p.ComposeVersion != "2.29.1" || !p.BuildxInstalled { + t.Errorf("compose/buildx do not need the daemon: %+v", p) + } +} + +func TestDesktopProfile(t *testing.T) { + facts := fact.NewSet() + info := `{"ServerVersion":"27.0.3","OSType":"linux","Architecture":"aarch64","OperatingSystem":"Docker Desktop","MemTotal":4102033408,"NCPU":4,"Runtimes":{"runc":{}},"SecurityOptions":["name=seccomp,profile=builtin","name=cgroupns"]}` + p := Probe(context.Background(), fakeRunner{ + "docker --version": "Docker version 27.0.3", + "docker info": info, + "docker context inspect": "unix:///Users/me/.docker/run/docker.sock", + "docker ps": "db-1\tshop\t0.0.0.0:5432->5432/tcp, :::5432->5432/tcp\nweb\t\t0.0.0.0:8000-8001->80-81/tcp", + "docker compose": "v2.29.1-desktop.1", + "docker buildx version": "github.com/docker/buildx v0.16.1 abc", + "docker buildx inspect": "Name: desktop-linux\nPlatforms: linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64", + }, facts) + if len(facts.Roots()) != 0 { + t.Fatalf("unexpected failures: %+v", facts.Roots()) + } + if p.Platform != "linux/arm64" || !p.IsDesktop() || p.MemTotal != 4102033408 { + t.Errorf("profile = %+v", p) + } + if native, emu := p.CanRun("linux/amd64"); native || !emu { + t.Error("desktop on arm64 should emulate amd64") + } + if native, emu := p.CanRun("linux/s390x"); native || emu { + t.Error("s390x is not available") + } + if holder, c := p.PortHolder("", 5432, "tcp"); c == nil || c.ComposeProject != "shop" { + t.Errorf("5432 holder = %q %+v", holder, c) + } + if _, c := p.PortHolder("", 8001, "tcp"); c == nil || c.Name != "web" { + t.Error("range ports not parsed") + } + if p.ComposeVersion != "2.29.1-desktop.1" { + t.Errorf("compose version = %q", p.ComposeVersion) + } +} + +func TestColimaKernelProbe(t *testing.T) { + facts := fact.NewSet() + info := `{"ServerVersion":"29.1.3","OSType":"linux","Architecture":"aarch64","OperatingSystem":"Ubuntu 24.04.4 LTS","MemTotal":2053578752,"NCPU":2,"DockerRootDir":"/var/lib/docker"}` + p := Probe(context.Background(), fakeRunner{ + "docker --version": "Docker version 29.8.1", + "docker context inspect": "unix:///Users/me/.colima/default/docker.sock", + "docker info": info, + "colima ssh -p default -- sh -c": "max_map_count=1048576\nunpriv_port_start=1024\nbinfmt=python3.12 qemu-i386 qemu-x86_64 register status\ndisk_free=95000000000", + "docker-compose": "5.5.1", + "docker compose": "ERR:unknown command", + "docker buildx": "ERR:unknown command", + }, facts) + if !p.EmulationKnown || len(p.Emulated) != 1 || p.Emulated[0] != "linux/amd64" || p.Emulators["linux/amd64"] != "qemu" { + t.Errorf("emulation = %v known=%v", p.Emulated, p.EmulationKnown) + } + if p.Sysctls["vm.max_map_count"] != 1048576 || !p.DiskExact || p.DiskFree != 95000000000 { + t.Errorf("kernel facts = %v disk=%d exact=%v", p.Sysctls, p.DiskFree, p.DiskExact) + } + if p.ComposeVersion != "5.5.1" || p.BuildxInstalled { + t.Errorf("compose=%q buildx=%v", p.ComposeVersion, p.BuildxInstalled) + } +} diff --git a/pkg/preflight/host/snapshot.go b/pkg/preflight/host/snapshot.go new file mode 100644 index 0000000..fcf2fb0 --- /dev/null +++ b/pkg/preflight/host/snapshot.go @@ -0,0 +1,123 @@ +package host + +import ( + "context" + "encoding/json" + "fmt" + "os" + osexec "os/exec" + "strconv" + "strings" + "time" + + "github.com/raphgm/container-doctor/internal/executor" + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +const snapshotVersion = 1 + +// Snapshot is a portable host profile. Preflight can run against a snapshot +// from another machine (a teammate's laptop, a CI runner, a server) to +// predict failures there without access to it. +type Snapshot struct { + Version int `json:"version"` + CapturedAt time.Time `json:"capturedAt"` + Hostname string `json:"hostname"` + Profile *Profile `json:"profile"` + Failures []fact.Failure `json:"failures,omitempty"` + ListeningPorts []string `json:"listeningPorts,omitempty"` +} + +// Capture probes this host and records it as a snapshot. +func Capture(ctx context.Context, run executor.Runner) *Snapshot { + facts := fact.NewSet() + p := Probe(ctx, run, facts) + name, _ := os.Hostname() + s := &Snapshot{ + Version: snapshotVersion, + CapturedAt: time.Now().UTC(), + Hostname: name, + Profile: p, + Failures: facts.All(), + } + if p.Local { + s.ListeningPorts = listeningPorts() + } + return s +} + +// Restore turns a snapshot back into a profile and fact set. Port checks use +// the ports that were listening when the snapshot was taken. +func (s *Snapshot) Restore() (*Profile, *fact.Set) { + facts := fact.NewSet() + for _, f := range s.Failures { + facts.Fail(f) + } + p := s.Profile + if p.Sysctls == nil { + p.Sysctls = map[string]int64{} + } + if p.LocalImages == nil { + p.LocalImages = map[string]bool{} + } + if p.PublishedPorts == nil { + p.PublishedPorts = map[string][]Container{} + } + if p.Local { + listening := map[string]bool{} + for _, lp := range s.ListeningPorts { + listening[lp] = true + } + p.PortProbe = func(_ string, port int, proto string) bool { + return listening[fmt.Sprintf("%d/%s", port, proto)] + } + p.Lsof = nil + } + return p, facts +} + +func LoadSnapshot(path string) (*Snapshot, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var s Snapshot + if err := json.Unmarshal(b, &s); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if s.Profile == nil { + return nil, fmt.Errorf("%s: not a container-doctor snapshot", path) + } + if s.Version > snapshotVersion { + return nil, fmt.Errorf("%s: snapshot version %d is newer than this container-doctor supports", path, s.Version) + } + return &s, nil +} + +// listeningPorts lists TCP ports with a listener on this machine. +func listeningPorts() []string { + out, err := osexec.Command("lsof", "-nP", "-iTCP", "-sTCP:LISTEN", "-Fn").Output() + if err != nil { + return nil + } + seen := map[string]bool{} + var ports []string + for _, line := range strings.Split(string(out), "\n") { + if !strings.HasPrefix(line, "n") { + continue + } + i := strings.LastIndex(line, ":") + if i < 0 { + continue + } + if _, err := strconv.Atoi(line[i+1:]); err != nil { + continue + } + key := line[i+1:] + "/tcp" + if !seen[key] { + seen[key] = true + ports = append(ports, key) + } + } + return ports +} diff --git a/pkg/preflight/preflight.go b/pkg/preflight/preflight.go new file mode 100644 index 0000000..99022aa --- /dev/null +++ b/pkg/preflight/preflight.go @@ -0,0 +1,199 @@ +// Package preflight predicts whether a container project will build and run +// on this host before anything is executed. It joins requirements extracted +// from the project with a capability profile of the host and live registry +// metadata, and reports each predicted failure once, at its root cause. +package preflight + +import ( + "context" + "errors" + "sort" + "sync" + "time" + + "github.com/raphgm/container-doctor/internal/executor" + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/project" + "github.com/raphgm/container-doctor/pkg/preflight/registry" + "github.com/raphgm/container-doctor/pkg/preflight/rules" +) + +type Options struct { + Dir string + Offline bool + + // Host, when set, replaces probing this machine: preflight predicts + // for the machine the snapshot was taken on. + Host *host.Snapshot + + Runner executor.Runner + Resolver registry.Resolver + Rules []rules.Rule +} + +// RootCause is a fact that could not be established, with every rule it +// blocked. Fixing it is what makes those rules checkable. +type RootCause struct { + fact.Failure + Blocks []string `json:"blocks"` +} + +type Report struct { + Project string `json:"project"` + Dir string `json:"dir"` + HostName string `json:"hostName,omitempty"` + Host *host.Profile `json:"host"` + Roots []RootCause `json:"rootCauses"` + Findings []rules.Finding `json:"findings"` + Passed []string `json:"passed"` + Duration time.Duration `json:"duration"` + Timestamp time.Time `json:"timestamp"` +} + +// Errors counts findings and root causes that will break the project. +func (r *Report) Errors() int { + n := 0 + for _, f := range r.Findings { + if f.Severity == fact.Error { + n++ + } + } + for _, rc := range r.Roots { + if rc.Severity == fact.Error { + n++ + } + } + return n +} + +func Run(ctx context.Context, opts Options) (*Report, error) { + start := time.Now() + if opts.Runner == nil { + opts.Runner = executor.New() + } + if opts.Resolver == nil { + opts.Resolver = registry.NewRemote() + } + if opts.Rules == nil { + opts.Rules = rules.All() + } + + var h *host.Profile + facts := fact.NewSet() + if opts.Host != nil { + h, facts = opts.Host.Restore() + } + proj, err := project.Load(ctx, opts.Dir, facts) + if err != nil { + return nil, err + } + if h == nil { + h = host.Probe(ctx, opts.Runner, facts) + } + + env := &rules.Env{Project: proj, Host: h, Facts: facts} + if opts.Offline { + facts.Fail(fact.Failure{ + Fact: fact.Registry, + Severity: fact.Info, + Summary: "Registry checks skipped (--offline)", + }) + } else { + env.Images = resolveImages(ctx, opts.Resolver, rules.Pulls(proj, h), facts) + } + + rep := &Report{Project: proj.Name, Dir: proj.Dir, Host: h, Timestamp: start} + if opts.Host != nil { + rep.HostName = opts.Host.Hostname + } + blocked := map[fact.ID][]string{} + for _, r := range opts.Rules { + if root, ok := blockingRoot(facts, r.Needs()); ok { + blocked[root.Fact] = append(blocked[root.Fact], r.ID()) + continue + } + found := r.Check(ctx, env) + if len(found) == 0 { + rep.Passed = append(rep.Passed, r.ID()) + } + rep.Findings = append(rep.Findings, found...) + } + + for _, root := range facts.Roots() { + rep.Roots = append(rep.Roots, RootCause{Failure: root, Blocks: blocked[root.Fact]}) + } + sort.SliceStable(rep.Findings, func(i, j int) bool { + return rank(rep.Findings[i].Severity) < rank(rep.Findings[j].Severity) + }) + rep.Duration = time.Since(start) + return rep, nil +} + +func blockingRoot(facts *fact.Set, needs []fact.ID) (fact.Failure, bool) { + for _, id := range needs { + if root, ok := facts.Root(id); ok { + return root, true + } + } + return fact.Failure{}, false +} + +// resolveImages fetches registry metadata for all pulls in parallel. If the +// network is down, that becomes one root cause rather than one error per +// image. +func resolveImages(ctx context.Context, res registry.Resolver, pulls []rules.Pull, facts *fact.Set) map[string]rules.ImageResult { + out := map[string]rules.ImageResult{} + var mu sync.Mutex + var wg sync.WaitGroup + sem := make(chan struct{}, 8) + + for _, p := range pulls { + key := rules.ImageKey(p.Ref, p.Platform) + mu.Lock() + if _, dup := out[key]; dup { + mu.Unlock() + continue + } + out[key] = rules.ImageResult{} + mu.Unlock() + + wg.Add(1) + go func(p rules.Pull, key string) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + cctx, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + img, err := res.Resolve(cctx, p.Ref, p.Platform) + mu.Lock() + out[key] = rules.ImageResult{Image: img, Err: err} + mu.Unlock() + }(p, key) + } + wg.Wait() + + for _, r := range out { + if errors.Is(r.Err, registry.ErrNetwork) { + facts.Fail(fact.Failure{ + Fact: fact.Registry, + Severity: fact.Warning, + Summary: "Container registries are unreachable", + Detail: r.Err.Error(), + Fix: "Check network/proxy settings. Pulls will fail unless images are already cached locally.", + }) + break + } + } + return out +} + +func rank(s fact.Severity) int { + switch s { + case fact.Error: + return 0 + case fact.Warning: + return 1 + } + return 2 +} diff --git a/pkg/preflight/preflight_test.go b/pkg/preflight/preflight_test.go new file mode 100644 index 0000000..5908ec1 --- /dev/null +++ b/pkg/preflight/preflight_test.go @@ -0,0 +1,128 @@ +package preflight + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +type daemonDown struct{} + +func (daemonDown) Run(_ context.Context, name string, args ...string) (string, error) { + cmd := name + " " + strings.Join(args, " ") + switch { + case strings.HasPrefix(cmd, "docker --version"): + return "Docker version 27.0.3", nil + case strings.HasPrefix(cmd, "docker context inspect"): + return "unix:///var/run/docker.sock", nil + case strings.HasPrefix(cmd, "docker compose version"): + return "2.29.1", nil + case strings.HasPrefix(cmd, "docker buildx version"): + return "github.com/docker/buildx v0.16.1 x", nil + } + return "", errors.New("Cannot connect to the Docker daemon at unix:///var/run/docker.sock") +} + +type offlineRegistry struct{} + +func (offlineRegistry) Resolve(context.Context, string, string) (*registry.Image, error) { + return nil, fmt.Errorf("%w: dial tcp: lookup registry-1.docker.io: no such host", registry.ErrNetwork) +} + +func sampleProject(t *testing.T) string { + dir := t.TempDir() + files := map[string]string{ + "compose.yaml": "services:\n web: {build: ., ports: [\"8080:80\"]}\n api: {image: node:20, ports: [\"8080:3000\"]}\n", + "Dockerfile": "FROM nginx\nCOPY dist /usr/share/nginx/html\n", + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + return dir +} + +func TestRootCausesBlockDependentsOnly(t *testing.T) { + rep, err := Run(context.Background(), Options{Dir: sampleProject(t), Runner: daemonDown{}, Resolver: offlineRegistry{}}) + if err != nil { + t.Fatal(err) + } + + roots := map[fact.ID][]string{} + for _, r := range rep.Roots { + roots[r.Fact] = r.Blocks + } + if len(roots) != 2 { + t.Fatalf("want 2 root causes (daemon, registry), got %+v", rep.Roots) + } + if !contains(roots[fact.Daemon], "resources.memory") || !contains(roots[fact.Daemon], "image.platform") { + t.Errorf("daemon should block memory and platform checks: %v", roots[fact.Daemon]) + } + if !contains(roots[fact.Registry], "image.exists") { + t.Errorf("registry should block image.exists: %v", roots[fact.Registry]) + } + + // Checks that need neither still run and find real problems. + var got []string + for _, f := range rep.Findings { + got = append(got, f.Rule) + } + for _, want := range []string{"build.context", "ports.conflict"} { + if !contains(got, want) { + t.Errorf("missing %s finding; got %v", want, got) + } + } + for _, blocked := range []string{"resources.memory", "image.platform", "image.exists"} { + if contains(got, blocked) || contains(rep.Passed, blocked) { + t.Errorf("%s ran although it is blocked", blocked) + } + } +} + +func TestSnapshotRoundTrip(t *testing.T) { + snap := host.Capture(context.Background(), daemonDown{}) + b, err := json.Marshal(snap) + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "host.json") + if err := os.WriteFile(path, b, 0o644); err != nil { + t.Fatal(err) + } + loaded, err := host.LoadSnapshot(path) + if err != nil { + t.Fatal(err) + } + rep, err := Run(context.Background(), Options{Dir: sampleProject(t), Host: loaded, Offline: true}) + if err != nil { + t.Fatal(err) + } + found := false + for _, r := range rep.Roots { + if r.Fact == fact.Daemon { + found = true + } + } + if !found { + t.Errorf("daemon failure should survive the snapshot: %+v", rep.Roots) + } +} + +func contains(xs []string, x string) bool { + for _, v := range xs { + if v == x { + return true + } + } + return false +} diff --git a/pkg/preflight/project/context.go b/pkg/preflight/project/context.go new file mode 100644 index 0000000..d7230a2 --- /dev/null +++ b/pkg/preflight/project/context.go @@ -0,0 +1,104 @@ +package project + +import ( + "os" + "path/filepath" + "strings" + + "github.com/moby/patternmatcher" + "github.com/moby/patternmatcher/ignorefile" +) + +// BuildContext answers whether COPY/ADD sources will be visible to the +// builder, applying .dockerignore the way BuildKit does. +type BuildContext struct { + Dir string + IgnoreFile string + patterns []string +} + +// OpenContext loads the ignore rules for a build. A Dockerfile-specific +// ignore file (.dockerignore) takes precedence over the +// context's .dockerignore, as in BuildKit. +func OpenContext(dir, dockerfile string) (*BuildContext, error) { + bc := &BuildContext{Dir: dir} + candidates := []string{filepath.Join(dir, ".dockerignore")} + if dockerfile != "" { + candidates = append([]string{dockerfile + ".dockerignore"}, candidates...) + } + for _, c := range candidates { + f, err := os.Open(c) + if err != nil { + continue + } + patterns, err := ignorefile.ReadAll(f) + f.Close() + if err != nil { + return nil, err + } + bc.IgnoreFile, bc.patterns = c, patterns + break + } + return bc, nil +} + +type SourceStatus int + +const ( + SourceOK SourceStatus = iota + SourceSkipped + SourceMissing + SourceIgnored + SourceOutside +) + +// Check reports whether src resolves to at least one file the builder will +// receive. For SourceIgnored it also returns the pattern responsible. +func (bc *BuildContext) Check(src string) (SourceStatus, string) { + if strings.Contains(src, "$") || strings.Contains(src, "://") || strings.HasPrefix(src, "git@") { + return SourceSkipped, "" + } + rel := filepath.Clean(strings.TrimPrefix(src, "/")) + if rel == ".." || strings.HasPrefix(rel, "../") { + return SourceOutside, "" + } + + matches, err := filepath.Glob(filepath.Join(bc.Dir, rel)) + if err != nil || len(matches) == 0 { + return SourceMissing, "" + } + var blocker string + for _, m := range matches { + r, _ := filepath.Rel(bc.Dir, m) + pattern := bc.excludedBy(filepath.ToSlash(r)) + if pattern == "" { + return SourceOK, "" + } + blocker = pattern + } + return SourceIgnored, blocker +} + +// excludedBy returns the ignore pattern that excludes rel, or "" when rel is +// sent to the builder. The last matching pattern wins, and a `!` pattern +// re-includes. +func (bc *BuildContext) excludedBy(rel string) string { + if rel == "." { + return "" + } + var last string + excluded := false + for _, p := range bc.patterns { + neg := strings.HasPrefix(p, "!") + body := strings.TrimPrefix(p, "!") + ok, err := patternmatcher.MatchesOrParentMatches(rel, []string{body}) + if err != nil || !ok { + continue + } + excluded, last = !neg, p + } + if !excluded { + return "" + } + return last +} diff --git a/pkg/preflight/project/dockerfile.go b/pkg/preflight/project/dockerfile.go new file mode 100644 index 0000000..56a5f81 --- /dev/null +++ b/pkg/preflight/project/dockerfile.go @@ -0,0 +1,275 @@ +package project + +import ( + "bytes" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/moby/buildkit/frontend/dockerfile/parser" +) + +// Dockerfile is the requirement view of a parsed Dockerfile. +type Dockerfile struct { + Path string + Syntax string + Stages []Stage + + // BuildKitFeatures lists syntax the legacy builder rejects. + BuildKitFeatures []Feature +} + +type Stage struct { + Name string + + // Base is the FROM reference after ARG expansion. + Base string + + // BaseStage is the index of the earlier stage Base refers to, or -1 when + // Base is an external image. + BaseStage int + + // Platform is the FROM --platform value after ARG expansion. It may + // still contain $BUILDPLATFORM or $TARGETPLATFORM. + Platform string + + Location Location + + // Copies are COPY/ADD instructions that read from the build context. + Copies []Copy + + // Deps are indices of other stages this stage reads from. + Deps []int +} + +type Copy struct { + Instruction string + Sources []string + Location Location +} + +// Unresolved reports whether Base still depends on an ARG with no value. +func (s Stage) Unresolved() bool { + return strings.Contains(s.Base, "$") +} + +// ParseDockerfile parses the file at path. args override ARG defaults, as +// `--build-arg` would. +func ParseDockerfile(path string, args map[string]string) (*Dockerfile, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return parseDockerfile(path, content, args) +} + +func parseDockerfile(path string, content []byte, args map[string]string) (*Dockerfile, error) { + res, err := parser.Parse(bytes.NewReader(content)) + if err != nil { + return nil, err + } + stages, metaArgs, err := instructions.Parse(res.AST, nil) + if err != nil { + return nil, err + } + + df := &Dockerfile{Path: path} + df.Syntax, _, _, _ = parser.DetectSyntax(content) + + vars := map[string]string{} + for _, a := range metaArgs { + for _, kv := range a.Args { + if kv.Value != nil { + vars[kv.Key] = *kv.Value + } + } + } + for k, v := range args { + vars[k] = v + } + + for _, node := range res.AST.Children { + if len(node.Heredocs) > 0 { + df.BuildKitFeatures = append(df.BuildKitFeatures, Feature{ + Name: "heredoc (< 0 { + add("exclude") + } +} + +// Reachable returns the indices of stages BuildKit actually builds for +// target (the last stage when target is empty). Unreachable stages are +// skipped by BuildKit, so their problems do not break the build. +func (df *Dockerfile) Reachable(target string) ([]int, error) { + if len(df.Stages) == 0 { + return nil, nil + } + start := len(df.Stages) - 1 + if target != "" { + start = -1 + for i, s := range df.Stages { + if strings.EqualFold(s.Name, target) { + start = i + } + } + if start < 0 { + return nil, fmt.Errorf("target stage %q not found", target) + } + } + + seen := map[int]bool{} + var visit func(int) + visit = func(i int) { + if seen[i] { + return + } + seen[i] = true + for _, d := range df.Stages[i].Deps { + visit(d) + } + } + visit(start) + + var out []int + for i := range df.Stages { + if seen[i] { + out = append(out, i) + } + } + return out, nil +} + +func stageRef(ref string, byName map[string]int, current int) (int, bool) { + if idx, ok := byName[strings.ToLower(ref)]; ok { + return idx, true + } + if n, err := strconv.Atoi(ref); err == nil && n >= 0 && n < current { + return n, true + } + return 0, false +} + +func firstLine(r []parser.Range) int { + if len(r) == 0 { + return 0 + } + return r[0].Start.Line +} + +var varRef = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?([-+])([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)`) + +// expand substitutes ARG values the way the Dockerfile frontend does for +// FROM lines. Variables with no value are left in place so callers can tell +// the reference is unresolved. +func expand(s string, vars map[string]string) string { + return varRef.ReplaceAllStringFunc(s, func(m string) string { + sub := varRef.FindStringSubmatch(m) + name, op, word := sub[1], sub[2], sub[3] + if name == "" { + name = sub[4] + } + v, ok := vars[name] + switch op { + case "-": + if !ok || v == "" { + return word + } + case "+": + if ok && v != "" { + return word + } + return "" + } + if !ok { + return m + } + return v + }) +} + +// expandKeepUnknown is expand, but it never touches the automatic platform +// ARGs, which only the builder can fill in. +func expandKeepUnknown(s string, vars map[string]string) string { + filtered := map[string]string{} + for k, v := range vars { + if !strings.HasSuffix(k, "PLATFORM") { + filtered[k] = v + } + } + return expand(s, filtered) +} diff --git a/pkg/preflight/project/load.go b/pkg/preflight/project/load.go new file mode 100644 index 0000000..50fa5a6 --- /dev/null +++ b/pkg/preflight/project/load.go @@ -0,0 +1,288 @@ +package project + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/dotenv" + "github.com/compose-spec/compose-go/v2/types" + "github.com/sirupsen/logrus" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +// composeFileNames is the lookup order Compose itself uses. +var composeFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yaml", "docker-compose.yml"} + +// Load extracts requirements from dir. A Compose file is used when present; +// otherwise a lone Dockerfile is treated as a single built service. Parse +// problems are returned as fact failures so rules depending on them are +// reported as blocked rather than silently skipped. +func Load(ctx context.Context, dir string, facts *fact.Set) (*Project, error) { + abs, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + if st, err := os.Stat(abs); err != nil || !st.IsDir() { + return nil, fmt.Errorf("%s is not a directory", dir) + } + + p := &Project{Dir: abs, Name: filepath.Base(abs), Env: environment(abs)} + + for _, name := range composeFileNames { + if _, err := os.Stat(filepath.Join(abs, name)); err == nil { + p.ComposeFile = filepath.Join(abs, name) + break + } + } + + if p.ComposeFile == "" { + dockerfile := filepath.Join(abs, "Dockerfile") + if _, err := os.Stat(dockerfile); err != nil { + return nil, fmt.Errorf("no compose file or Dockerfile found in %s", abs) + } + svc := &Service{ + Name: p.Name, + Replicas: 1, + Build: &Build{Context: abs, DockerfilePath: dockerfile}, + Location: Location{File: dockerfile}, + } + loadDockerfile(svc.Build, facts) + p.Services = append(p.Services, svc) + return p, nil + } + + raw, err := os.ReadFile(p.ComposeFile) + if err != nil { + return nil, err + } + p.EnvRefs = scanEnvRefs(p.ComposeFile, raw) + p.ComposeFeatures = scanComposeFeatures(p.ComposeFile, raw) + + cp, err := loadCompose(ctx, abs, p.ComposeFile, nil) + if err != nil { + // Missing ${VAR:?} values are reported precisely by the env rule; + // keep analysing the rest of the file instead of stopping here. + if placeholders := missingRequired(p); len(placeholders) > 0 { + cp, err = loadCompose(ctx, abs, p.ComposeFile, placeholders) + } + } + if err != nil { + facts.Fail(fact.Failure{ + Fact: fact.ComposeFile, + Severity: fact.Error, + Summary: "Compose file cannot be loaded", + Detail: err.Error(), + Fix: "Fix the error above; `docker compose config` shows the same message.", + }) + return p, nil + } + p.Name = cp.Name + + lines := serviceLines(raw) + for _, name := range cp.ServiceNames() { + sc := cp.Services[name] + svc := convertService(sc) + svc.Location = Location{File: p.ComposeFile, Line: lines[name]} + if svc.Build != nil { + loadDockerfile(svc.Build, facts) + } + for _, ef := range sc.EnvFiles { + p.EnvFiles = append(p.EnvFiles, EnvFile{Service: name, Path: ef.Path, Required: bool(ef.Required)}) + } + p.Services = append(p.Services, svc) + } + return p, nil +} + +func loadCompose(ctx context.Context, dir, file string, extraEnv map[string]string) (*types.Project, error) { + // compose-go logs interpolation warnings; preflight reports those itself. + logrus.SetOutput(io.Discard) + + fns := []cli.ProjectOptionsFn{ + cli.WithWorkingDirectory(dir), + cli.WithOsEnv, + cli.WithEnvFiles(), + cli.WithDotEnv, + } + if len(extraEnv) > 0 { + var kv []string + for k, v := range extraEnv { + kv = append(kv, k+"="+v) + } + fns = append(fns, cli.WithEnv(kv)) + } + opts, err := cli.NewProjectOptions([]string{file}, fns...) + if err != nil { + return nil, err + } + return cli.ProjectFromOptions(ctx, opts) +} + +// missingRequired returns placeholder values for ${VAR:?} references that +// have no value. +func missingRequired(p *Project) map[string]string { + out := map[string]string{} + for _, r := range p.EnvRefs { + if _, set := p.Env[r.Name]; r.Required && !set { + out[r.Name] = "preflight-placeholder" + } + } + return out +} + +func convertService(sc types.ServiceConfig) *Service { + svc := &Service{ + Name: sc.Name, + Platform: sc.Platform, + MemLimit: int64(sc.MemLimit), + Replicas: 1, + Environment: map[string]string{}, + } + if sc.Build == nil { + svc.Image = sc.Image + } else { + b := &Build{ + Context: sc.Build.Context, + Target: sc.Build.Target, + Platforms: sc.Build.Platforms, + Args: map[string]string{}, + } + for k, v := range sc.Build.Args { + if v != nil { + b.Args[k] = *v + } + } + switch { + case sc.Build.DockerfileInline != "": + b.DockerfilePath = "" + case filepath.IsAbs(sc.Build.Dockerfile): + b.DockerfilePath = sc.Build.Dockerfile + default: + df := sc.Build.Dockerfile + if df == "" { + df = "Dockerfile" + } + b.DockerfilePath = filepath.Join(sc.Build.Context, df) + } + svc.Build = b + } + + if sc.Scale != nil { + svc.Replicas = *sc.Scale + } + if d := sc.Deploy; d != nil { + if d.Replicas != nil { + svc.Replicas = *d.Replicas + } + if lim := d.Resources.Limits; lim != nil { + if l := int64(lim.MemoryBytes); l > 0 && (svc.MemLimit == 0 || l < svc.MemLimit) { + svc.MemLimit = l + } + } + if r := d.Resources.Reservations; r != nil { + svc.MemReservation = int64(r.MemoryBytes) + for _, dev := range r.Devices { + if hasGPU(dev.Capabilities) { + svc.GPU = true + } + } + } + } + if int64(sc.MemReservation) > svc.MemReservation { + svc.MemReservation = int64(sc.MemReservation) + } + if len(sc.Gpus) > 0 { + svc.GPU = true + } + + for k, v := range sc.Environment { + if v != nil { + svc.Environment[k] = *v + } + } + + for _, pc := range sc.Ports { + if pc.Published == "" { + continue + } + start, end, err := parseRange(pc.Published) + if err != nil { + continue + } + proto := pc.Protocol + if proto == "" { + proto = "tcp" + } + svc.Ports = append(svc.Ports, Port{HostIP: pc.HostIP, Start: start, End: end, Target: pc.Target, Protocol: proto}) + } + + for _, v := range sc.Volumes { + if v.Type != types.VolumeTypeBind { + continue + } + create := true + if v.Bind != nil { + create = bool(v.Bind.CreateHostPath) + } + svc.Binds = append(svc.Binds, Bind{Source: v.Source, Target: v.Target, CreateHostPath: create}) + } + return svc +} + +func hasGPU(caps []string) bool { + for _, c := range caps { + if c == "gpu" { + return true + } + } + return false +} + +func parseRange(s string) (int, int, error) { + lo, hi, found := strings.Cut(s, "-") + start, err := strconv.Atoi(lo) + if err != nil { + return 0, 0, err + } + if !found { + return start, start, nil + } + end, err := strconv.Atoi(hi) + return start, end, err +} + +func loadDockerfile(b *Build, facts *fact.Set) { + if b.DockerfilePath == "" { + return + } + df, err := ParseDockerfile(b.DockerfilePath, b.Args) + if err != nil { + b.ParseErr = err + return + } + b.Dockerfile = df +} + +// environment is what Compose interpolates with: the .env file, overridden +// by the process environment. +func environment(dir string) map[string]string { + env := map[string]string{} + if dot, err := dotenv.GetEnvFromFile(map[string]string{}, []string{filepath.Join(dir, ".env")}); err == nil { + for k, v := range dot { + env[k] = v + } + } + for _, kv := range os.Environ() { + if k, v, ok := strings.Cut(kv, "="); ok { + env[k] = v + } + } + return env +} diff --git a/pkg/preflight/project/project.go b/pkg/preflight/project/project.go new file mode 100644 index 0000000..daf4bb3 --- /dev/null +++ b/pkg/preflight/project/project.go @@ -0,0 +1,128 @@ +// Package project extracts what a container project requires from the host: +// platforms, memory, ports, bind mounts, build inputs and tool features. It +// reads Compose files and Dockerfiles with the same parsers Docker uses. +package project + +import ( + "fmt" + "path/filepath" +) + +// Location points at the line in a project file that created a requirement. +type Location struct { + File string + Line int +} + +func (l Location) String() string { + if l.File == "" { + return "" + } + if l.Line == 0 { + return filepath.Base(l.File) + } + return fmt.Sprintf("%s:%d", filepath.Base(l.File), l.Line) +} + +// Project is the set of requirements extracted from one directory. +type Project struct { + Dir string + Name string + ComposeFile string + + Services []*Service + + // ComposeFeatures lists Compose-file syntax that needs a minimum + // Compose version. + ComposeFeatures []Feature + + // EnvRefs lists every variable interpolated in the Compose file. + EnvRefs []EnvRef + + // Env is the environment Compose would interpolate with (OS + .env). + Env map[string]string + + // EnvFiles lists env_file entries referenced by services. + EnvFiles []EnvFile +} + +type Service struct { + Name string + + // Image is the image Compose pulls. It is empty when the service is + // built and never pulled. + Image string + + Build *Build + + // Platform is the explicit `platform:` value, if any. + Platform string + + MemLimit int64 + MemReservation int64 + Replicas int + + Ports []Port + Binds []Bind + + GPU bool + + Environment map[string]string + + Location Location +} + +type Port struct { + HostIP string + Start int + End int + Target uint32 + Protocol string +} + +func (p Port) String() string { + host := fmt.Sprint(p.Start) + if p.End != p.Start { + host = fmt.Sprintf("%d-%d", p.Start, p.End) + } + if p.HostIP != "" { + host = p.HostIP + ":" + host + } + return fmt.Sprintf("%s->%d/%s", host, p.Target, p.Protocol) +} + +type Bind struct { + Source string + Target string + CreateHostPath bool +} + +type Build struct { + Context string + DockerfilePath string + Dockerfile *Dockerfile + ParseErr error + Target string + Platforms []string + Args map[string]string +} + +// Feature is syntax that only works with a new enough tool. +type Feature struct { + Name string + MinVersion string + Location Location +} + +type EnvRef struct { + Name string + HasDefault bool + Required bool + Location Location +} + +type EnvFile struct { + Service string + Path string + Required bool +} diff --git a/pkg/preflight/project/project_test.go b/pkg/preflight/project/project_test.go new file mode 100644 index 0000000..7fef9f8 --- /dev/null +++ b/pkg/preflight/project/project_test.go @@ -0,0 +1,240 @@ +package project + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +func write(t *testing.T, dir string, files map[string]string) { + t.Helper() + for name, content := range files { + p := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } +} + +func TestDockerfileStagesAndReachability(t *testing.T) { + dir := t.TempDir() + write(t, dir, map[string]string{"Dockerfile": `ARG NODE=20 +FROM node:${NODE}-alpine AS deps +COPY package.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci + +FROM golang:1.22 AS unused +COPY missing.go ./ + +FROM --platform=$BUILDPLATFORM deps AS build +COPY --chmod=755 src ./src + +FROM gcr.io/distroless/nodejs20 +COPY --from=build /app /app +`}) + + df, err := ParseDockerfile(filepath.Join(dir, "Dockerfile"), nil) + if err != nil { + t.Fatal(err) + } + if got := df.Stages[0].Base; got != "node:20-alpine" { + t.Errorf("ARG not expanded: base = %q", got) + } + if df.Stages[2].BaseStage != 0 { + t.Errorf("stage 2 should derive from stage 0, got %d", df.Stages[2].BaseStage) + } + if df.Stages[2].Platform != "$BUILDPLATFORM" { + t.Errorf("platform ARG must be left for the builder, got %q", df.Stages[2].Platform) + } + + reach, _ := df.Reachable("") + if want := []int{0, 2, 3}; !equal(reach, want) { + t.Errorf("reachable = %v; want %v (stage 1 is never built)", reach, want) + } + + var names []string + for _, f := range df.BuildKitFeatures { + names = append(names, f.Name) + } + if !contains(names, "RUN --mount") || !contains(names, "COPY --chmod") { + t.Errorf("BuildKit features = %v", names) + } +} + +func TestBuildArgOverridesDefault(t *testing.T) { + dir := t.TempDir() + write(t, dir, map[string]string{"Dockerfile": "ARG TAG=1\nFROM alpine:${TAG}\n"}) + df, err := ParseDockerfile(filepath.Join(dir, "Dockerfile"), map[string]string{"TAG": "3.20"}) + if err != nil { + t.Fatal(err) + } + if df.Stages[0].Base != "alpine:3.20" { + t.Errorf("base = %q", df.Stages[0].Base) + } +} + +func TestLoadCompose(t *testing.T) { + dir := t.TempDir() + write(t, dir, map[string]string{ + "compose.yaml": `include: + - other.yaml +services: + db: + image: postgres:16 + mem_limit: 512m + ports: ["5432:5432"] + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD:?set a password} + web: + build: . + platform: linux/amd64 + ports: ["8000-8001:80"] + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - data:/data + deploy: + replicas: 2 + resources: + reservations: + devices: [{capabilities: [gpu]}] + develop: + watch: [{path: ., action: rebuild}] + depends_on: + db: {condition: service_started, required: false} + environment: + MODE: ${MODE:-dev} + TOKEN: $API_TOKEN +volumes: {data: {}} +`, + "other.yaml": "services: {}\n", + "Dockerfile": "FROM nginx:1.27\n", + ".env": "DB_PASSWORD=x\n", + }) + + facts := fact.NewSet() + p, err := Load(context.Background(), dir, facts) + if err != nil { + t.Fatal(err) + } + if facts.Failed(fact.ComposeFile) { + r, _ := facts.Root(fact.ComposeFile) + t.Fatalf("compose failed to load: %s", r.Detail) + } + if len(p.Services) != 2 { + t.Fatalf("services = %d", len(p.Services)) + } + db, web := p.Services[0], p.Services[1] + if db.Image != "postgres:16" || db.MemLimit != 512*1024*1024 { + t.Errorf("db = %+v", db) + } + if web.Image != "" || web.Build == nil || web.Build.Dockerfile == nil { + t.Fatalf("web build not parsed: %+v", web) + } + if web.Replicas != 2 || !web.GPU || web.Platform != "linux/amd64" { + t.Errorf("web = replicas %d gpu %v platform %q", web.Replicas, web.GPU, web.Platform) + } + if len(web.Ports) != 1 || web.Ports[0].Start != 8000 || web.Ports[0].End != 8001 { + t.Errorf("ports = %+v", web.Ports) + } + if len(web.Binds) != 1 || web.Binds[0].Source != filepath.Join(dir, "nginx.conf") { + t.Errorf("binds = %+v", web.Binds) + } + + var features []string + for _, f := range p.ComposeFeatures { + features = append(features, f.Name) + } + for _, want := range []string{"include", "services.web.develop", "services.web.depends_on.required"} { + if !contains(features, want) { + t.Errorf("missing compose feature %q in %v", want, features) + } + } + + refs := map[string]EnvRef{} + for _, r := range p.EnvRefs { + refs[r.Name] = r + } + if !refs["DB_PASSWORD"].Required || !refs["MODE"].HasDefault || refs["API_TOKEN"].HasDefault { + t.Errorf("env refs = %+v", refs) + } + if p.Env["DB_PASSWORD"] != "x" { + t.Error(".env not read") + } +} + +func TestBrokenComposeIsAFactFailure(t *testing.T) { + dir := t.TempDir() + write(t, dir, map[string]string{"compose.yaml": "services:\n a:\n image: [not, a, string]\n"}) + facts := fact.NewSet() + if _, err := Load(context.Background(), dir, facts); err != nil { + t.Fatal(err) + } + if !facts.Failed(fact.ComposeFile) { + t.Fatal("expected compose file failure") + } +} + +func equal(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func contains(xs []string, x string) bool { + for _, v := range xs { + if v == x { + return true + } + } + return false +} + +func TestBuildContextCheck(t *testing.T) { + dir := t.TempDir() + write(t, dir, map[string]string{ + "Dockerfile": "FROM alpine\n", + ".dockerignore": "*.env\nsecrets/\nnode_modules\n!keep.env\n", + "app.py": "", + "prod.env": "", + "keep.env": "", + "secrets/key": "", + }) + bc, err := OpenContext(dir, filepath.Join(dir, "Dockerfile")) + if err != nil { + t.Fatal(err) + } + cases := map[string]SourceStatus{ + "app.py": SourceOK, + "./app.py": SourceOK, + "*.py": SourceOK, + "prod.env": SourceIgnored, + "keep.env": SourceOK, + "secrets/key": SourceIgnored, + "secrets": SourceIgnored, + "missing.txt": SourceMissing, + "../outside": SourceOutside, + "$APP_DIR": SourceSkipped, + "https://x.y/z": SourceSkipped, + ".": SourceOK, + } + for src, want := range cases { + if got, pattern := bc.Check(src); got != want { + t.Errorf("Check(%q) = %v (%q); want %v", src, got, pattern, want) + } + } + if _, pattern := bc.Check("prod.env"); pattern != "*.env" { + t.Errorf("blocking pattern = %q; want *.env", pattern) + } +} diff --git a/pkg/preflight/project/scan.go b/pkg/preflight/project/scan.go new file mode 100644 index 0000000..e1f2b99 --- /dev/null +++ b/pkg/preflight/project/scan.go @@ -0,0 +1,142 @@ +package project + +import ( + "regexp" + "strings" + + "gopkg.in/yaml.v3" +) + +// composeMinVersions maps Compose-file syntax to the first Docker Compose v2 +// release that accepts it (from the docker/compose release notes). Older +// versions reject the file or silently ignore the key. +var composeMinVersions = struct { + topLevel map[string]string + service map[string]string + build map[string]string + dependsOn map[string]string +}{ + topLevel: map[string]string{ + "include": "2.20.0", + "models": "2.38.0", + }, + service: map[string]string{ + "develop": "2.22.0", + "gpus": "2.30.0", + "post_start": "2.30.0", + "pre_stop": "2.30.0", + }, + build: map[string]string{ + "additional_contexts": "2.17.0", + }, + dependsOn: map[string]string{ + "restart": "2.17.0", + "required": "2.20.0", + }, +} + +// scanComposeFeatures walks the raw YAML, keeping line numbers that the typed +// loader discards. +func scanComposeFeatures(file string, raw []byte) []Feature { + var doc yaml.Node + if yaml.Unmarshal(raw, &doc) != nil || len(doc.Content) == 0 { + return nil + } + root := doc.Content[0] + var out []Feature + add := func(name, version string, n *yaml.Node) { + out = append(out, Feature{Name: name, MinVersion: version, Location: Location{File: file, Line: n.Line}}) + } + + for key, val := range mappingPairs(root) { + if v, ok := composeMinVersions.topLevel[key.Value]; ok { + add(key.Value, v, key) + } + if key.Value != "services" { + continue + } + for svcKey, svc := range mappingPairs(val) { + for k, v := range mappingPairs(svc) { + if ver, ok := composeMinVersions.service[k.Value]; ok { + add("services."+svcKey.Value+"."+k.Value, ver, k) + } + switch k.Value { + case "build": + for bk := range mappingPairs(v) { + if ver, ok := composeMinVersions.build[bk.Value]; ok { + add("services."+svcKey.Value+".build."+bk.Value, ver, bk) + } + } + case "depends_on": + for _, dep := range mappingPairs(v) { + for dk := range mappingPairs(dep) { + if ver, ok := composeMinVersions.dependsOn[dk.Value]; ok { + add("services."+svcKey.Value+".depends_on."+dk.Value, ver, dk) + } + } + } + } + } + } + } + return out +} + +// mappingPairs iterates key/value nodes of a YAML mapping in file order. +func mappingPairs(n *yaml.Node) func(func(*yaml.Node, *yaml.Node) bool) { + return func(yield func(*yaml.Node, *yaml.Node) bool) { + if n == nil || n.Kind != yaml.MappingNode { + return + } + for i := 0; i+1 < len(n.Content); i += 2 { + if !yield(n.Content[i], n.Content[i+1]) { + return + } + } + } +} + +// serviceLines maps each service name to the line defining it. +func serviceLines(raw []byte) map[string]int { + out := map[string]int{} + var doc yaml.Node + if yaml.Unmarshal(raw, &doc) != nil || len(doc.Content) == 0 { + return out + } + for key, val := range mappingPairs(doc.Content[0]) { + if key.Value == "services" { + for svc := range mappingPairs(val) { + out[svc.Value] = svc.Line + } + } + } + return out +} + +var interpolation = regexp.MustCompile(`\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)(:?[-?+])?[^}]*\}|\$([A-Za-z_][A-Za-z0-9_]*)`) + +// scanEnvRefs finds every ${VAR} Compose will interpolate. +func scanEnvRefs(file string, raw []byte) []EnvRef { + var out []EnvRef + for i, line := range strings.Split(string(raw), "\n") { + if strings.HasPrefix(strings.TrimSpace(line), "#") { + continue + } + for _, m := range interpolation.FindAllStringSubmatch(line, -1) { + if m[0] == "$$" { + continue + } + name, op := m[1], m[2] + if name == "" { + name = m[3] + } + out = append(out, EnvRef{ + Name: name, + HasDefault: strings.HasSuffix(op, "-") || strings.HasSuffix(op, "+"), + Required: strings.HasSuffix(op, "?"), + Location: Location{File: file, Line: i + 1}, + }) + } + } + return out +} diff --git a/pkg/preflight/registry/live_test.go b/pkg/preflight/registry/live_test.go new file mode 100644 index 0000000..fdd9643 --- /dev/null +++ b/pkg/preflight/registry/live_test.go @@ -0,0 +1,63 @@ +package registry + +import ( + "context" + "errors" + "os" + "testing" + "time" +) + +// Hits real registries; run with PREFLIGHT_LIVE=1. +func TestLiveResolve(t *testing.T) { + if os.Getenv("PREFLIGHT_LIVE") == "" { + t.Skip("set PREFLIGHT_LIVE=1 to query real registries") + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + r := NewRemote() + + img, err := r.Resolve(ctx, "postgres:16", "linux/arm64") + if err != nil { + t.Fatal(err) + } + t.Logf("postgres:16 index=%v platforms=%v size=%d", img.Index, img.Platforms, img.CompressedSize) + if !img.Index || img.CompressedSize == 0 { + t.Error("expected multi-arch index with arm64 size") + } + + img, err = r.Resolve(ctx, "mcr.microsoft.com/mssql/server:2022-latest", "linux/arm64") + if err != nil { + t.Fatal(err) + } + t.Logf("mssql index=%v platforms=%v size=%d", img.Index, img.Platforms, img.CompressedSize) + + _, err = r.Resolve(ctx, "postgres:this-tag-does-not-exist-123", "linux/arm64") + t.Logf("missing tag: %v", err) + if !errors.Is(err, ErrNotFound) { + t.Errorf("want ErrNotFound, got %v", err) + } + _, err = r.Resolve(ctx, "raphgm/definitely-not-a-repo-xyz:1", "linux/arm64") + t.Logf("missing repo: %v", err) +} + +func TestNormalize(t *testing.T) { + cases := map[string]string{ + "linux/arm64/v8": "linux/arm64", + "Linux/aarch64": "linux/arm64", + "linux/x86_64": "linux/amd64", + "linux/arm/v7": "linux/arm/v7", + "linux/amd64/": "linux/amd64", + } + for in, want := range cases { + if got := Normalize(in); got != want { + t.Errorf("Normalize(%q) = %q; want %q", in, got, want) + } + } + if Compatible("linux/amd64", "linux/arm64") { + t.Error("amd64 must not satisfy arm64") + } + if !Compatible("linux/arm/v7", "linux/arm") { + t.Error("variant-less want should accept v7") + } +} diff --git a/pkg/preflight/registry/registry.go b/pkg/preflight/registry/registry.go new file mode 100644 index 0000000..3dbb079 --- /dev/null +++ b/pkg/preflight/registry/registry.go @@ -0,0 +1,256 @@ +// Package registry reads image metadata straight from registries, without +// pulling, so preflight can see which platforms an image ships and how big it +// is before Docker ever tries. +package registry + +import ( + "context" + "errors" + "fmt" + "net" + "net/http" + "strings" + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// Image describes an image reference as the registry sees it. +type Image struct { + Ref string + + // Index is true for a multi-platform manifest list. A single-platform + // image is pulled regardless of host platform; an index without a + // matching entry makes `docker pull` fail. + Index bool + + // Platforms are normalized "os/arch[/variant]" strings. + Platforms []string + + // CompressedSize is the download size for the requested platform, or 0 + // when that platform is not available. + CompressedSize int64 +} + +var ( + ErrNotFound = errors.New("image not found in registry") + ErrUnauthorized = errors.New("registry denied access") + ErrNetwork = errors.New("registry unreachable") +) + +type Resolver interface { + Resolve(ctx context.Context, ref, platform string) (*Image, error) +} + +// Remote resolves against real registries using the user's Docker +// credentials. Results are cached per reference and platform. +type Remote struct { + mu sync.Mutex + cache map[string]result + + keychain *fallbackKeychain +} + +type result struct { + img *Image + err error +} + +func NewRemote() *Remote { + return &Remote{cache: map[string]result{}, keychain: &fallbackKeychain{}} +} + +// CredentialError returns the first error hit while reading Docker +// credentials, e.g. a credsStore helper that is configured but not +// installed. Docker itself fails pulls in that state. +func (r *Remote) CredentialError() error { + r.keychain.mu.Lock() + defer r.keychain.mu.Unlock() + return r.keychain.err +} + +// fallbackKeychain uses the Docker credentials when they can be read and +// falls back to anonymous access, remembering why. +type fallbackKeychain struct { + mu sync.Mutex + err error +} + +func (k *fallbackKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { + a, err := authn.DefaultKeychain.Resolve(target) + if err != nil { + k.mu.Lock() + if k.err == nil { + k.err = err + } + k.mu.Unlock() + return authn.Anonymous, nil + } + return a, nil +} + +func (r *Remote) Resolve(ctx context.Context, ref, platform string) (*Image, error) { + key := ref + "|" + platform + r.mu.Lock() + if c, ok := r.cache[key]; ok { + r.mu.Unlock() + return c.img, c.err + } + r.mu.Unlock() + + img, err := r.resolve(ctx, ref, platform) + + r.mu.Lock() + r.cache[key] = result{img, err} + r.mu.Unlock() + return img, err +} + +func (r *Remote) resolve(ctx context.Context, ref, platform string) (*Image, error) { + parsed, err := name.ParseReference(ref) + if err != nil { + return nil, fmt.Errorf("%w: invalid reference %q: %v", ErrNotFound, ref, err) + } + opts := []remote.Option{ + remote.WithContext(ctx), + remote.WithAuthFromKeychain(r.keychain), + } + desc, err := remote.Get(parsed, opts...) + if err != nil { + return nil, classify(err) + } + + out := &Image{Ref: ref} + want := Normalize(platform) + + if desc.MediaType.IsIndex() { + out.Index = true + idx, err := desc.ImageIndex() + if err != nil { + return nil, classify(err) + } + im, err := idx.IndexManifest() + if err != nil { + return nil, classify(err) + } + var match *v1.Descriptor + for i, m := range im.Manifests { + if m.Platform == nil || m.Platform.OS == "unknown" { + continue // attestation manifests + } + p := Normalize(m.Platform.String()) + out.Platforms = append(out.Platforms, p) + if match == nil && Compatible(p, want) { + match = &im.Manifests[i] + } + } + if match != nil { + if img, err := idx.Image(match.Digest); err == nil { + out.CompressedSize = layerSize(img) + } + } + return out, nil + } + + img, err := desc.Image() + if err != nil { + return nil, classify(err) + } + cfg, err := img.ConfigFile() + if err != nil { + return nil, classify(err) + } + p := Normalize(cfg.OS + "/" + cfg.Architecture + "/" + cfg.Variant) + out.Platforms = []string{p} + out.CompressedSize = layerSize(img) + return out, nil +} + +func layerSize(img v1.Image) int64 { + m, err := img.Manifest() + if err != nil { + return 0 + } + var total int64 + for _, l := range m.Layers { + total += l.Size + } + return total +} + +func classify(err error) error { + var te *transport.Error + if errors.As(err, &te) { + switch te.StatusCode { + case http.StatusNotFound: + return fmt.Errorf("%w: %v", ErrNotFound, err) + case http.StatusUnauthorized, http.StatusForbidden: + // Docker Hub answers 401 for repositories that do not exist + // as well as for private ones. + return fmt.Errorf("%w: %v", ErrUnauthorized, err) + } + for _, d := range te.Errors { + if d.Code == transport.ManifestUnknownErrorCode || d.Code == transport.NameUnknownErrorCode { + return fmt.Errorf("%w: %v", ErrNotFound, err) + } + } + } + var ne net.Error + var dns *net.DNSError + if errors.As(err, &ne) || errors.As(err, &dns) || errors.Is(err, context.DeadlineExceeded) { + return fmt.Errorf("%w: %v", ErrNetwork, err) + } + return err +} + +// Canonical returns the fully qualified form of ref, so "postgres" and +// "docker.io/library/postgres:latest" compare equal. +func Canonical(ref string) string { + parsed, err := name.ParseReference(ref) + if err != nil { + return ref + } + return parsed.Name() +} + +// Normalize maps platform spellings to one canonical form. +func Normalize(p string) string { + p = strings.ToLower(strings.Trim(p, "/")) + parts := strings.Split(p, "/") + if len(parts) < 2 { + return p + } + arch := parts[1] + switch arch { + case "x86_64", "x86-64": + arch = "amd64" + case "aarch64": + arch = "arm64" + } + variant := "" + if len(parts) > 2 { + variant = parts[2] + } + if arch == "arm64" && variant == "v8" { + variant = "" + } + if variant == "" { + return parts[0] + "/" + arch + } + return parts[0] + "/" + arch + "/" + variant +} + +// Compatible reports whether an image built for have runs natively on want. +func Compatible(have, want string) bool { + have, want = Normalize(have), Normalize(want) + if have == want { + return true + } + // linux/arm/v7 and friends: accept a missing variant on either side. + hp, wp := strings.Split(have, "/"), strings.Split(want, "/") + return len(hp) >= 2 && len(wp) >= 2 && hp[0] == wp[0] && hp[1] == wp[1] && (len(hp) == 2 || len(wp) == 2) +} diff --git a/pkg/preflight/rules/build.go b/pkg/preflight/rules/build.go new file mode 100644 index 0000000..0577842 --- /dev/null +++ b/pkg/preflight/rules/build.go @@ -0,0 +1,250 @@ +package rules + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/project" +) + +var buildContextRule = rule{ + id: "build.context", + title: "Build inputs exist in the build context", + needs: []fact.ID{fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + for _, svc := range env.Project.Services { + b := svc.Build + if b == nil { + continue + } + f := func(sev fact.Severity, title, fix, loc, predicts string, evidence ...string) { + out = append(out, Finding{Rule: "build.context", Severity: sev, Service: svc.Name, Title: title, Fix: fix, Location: loc, Predicts: predicts, Evidence: evidence}) + } + if st, err := os.Stat(b.Context); err != nil || !st.IsDir() { + f(fact.Error, "build context "+rel(env, b.Context)+" does not exist", "Fix `build.context` in the Compose file.", svc.Location.String(), "unable to prepare context: path not found") + continue + } + if b.DockerfilePath == "" { + continue + } + if _, err := os.Stat(b.DockerfilePath); err != nil { + f(fact.Error, "Dockerfile "+rel(env, b.DockerfilePath)+" does not exist", "Fix `build.dockerfile`; it is relative to the build context.", svc.Location.String(), "failed to read dockerfile: open Dockerfile: no such file or directory") + continue + } + if b.ParseErr != nil { + f(fact.Error, "Dockerfile cannot be parsed", "Fix the syntax error.", rel(env, b.DockerfilePath), "dockerfile parse error", b.ParseErr.Error()) + continue + } + df := b.Dockerfile + reach, err := df.Reachable(b.Target) + if err != nil { + f(fact.Error, fmt.Sprintf("build target %q is not a stage in the Dockerfile", b.Target), "Fix `build.target`.", svc.Location.String(), "target stage \""+b.Target+"\" could not be found") + continue + } + + bc, err := project.OpenContext(b.Context, b.DockerfilePath) + if err != nil { + f(fact.Warning, ".dockerignore cannot be read", "", rel(env, b.Context), "", err.Error()) + continue + } + // The two builders word a missing COPY source differently. + notFound := func(src string) string { + if env.Host.BuildxInstalled && !env.Host.BuildKitDisabled { + return `failed to compute cache key: "/` + strings.TrimPrefix(src, "/") + `": not found` + } + return "COPY failed: file not found in build context or excluded by .dockerignore: stat " + strings.TrimPrefix(src, "/") + ": file does not exist" + } + for _, i := range reach { + st := df.Stages[i] + if st.Unresolved() { + f(fact.Error, "base image "+st.Base+" depends on an ARG with no value", "Give the ARG a default before the first FROM, or pass it in `build.args`.", st.Location.String(), "invalid reference format") + } + for _, cp := range st.Copies { + for _, src := range cp.Sources { + status, pattern := bc.Check(src) + switch status { + case project.SourceMissing: + f(fact.Error, fmt.Sprintf("%s source %q not found in build context", cp.Instruction, src), + "Create the file, fix the path (it is relative to "+rel(env, b.Context)+"), or run the step that generates it first.", + cp.Location.String(), notFound(src), + "context: "+rel(env, b.Context)) + case project.SourceIgnored: + f(fact.Error, fmt.Sprintf("%s source %q is excluded by .dockerignore", cp.Instruction, src), + fmt.Sprintf("Remove or negate the pattern %q in %s.", pattern, rel(env, bc.IgnoreFile)), + cp.Location.String(), notFound(src), + fmt.Sprintf("%s: pattern %q", rel(env, bc.IgnoreFile), pattern)) + case project.SourceOutside: + f(fact.Error, fmt.Sprintf("%s source %q is outside the build context", cp.Instruction, src), + "Move the file into the context, widen `build.context`, or use `build.additional_contexts`.", + cp.Location.String(), "forbidden path outside the build context") + } + } + } + } + } + return out + }, +} + +var buildKitRule = rule{ + id: "build.buildkit", + title: "Builder supports the Dockerfile syntax", + needs: []fact.ID{fact.DockerCLI, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + if h.BuildxInstalled && !h.BuildKitDisabled { + return nil + } + reason := "host: docker buildx plugin is not installed, so `docker build` falls back to the legacy builder" + fix := "Install the buildx plugin (`docker-buildx-plugin` on Linux; bundled with Docker Desktop)." + if h.BuildKitDisabled { + reason = "host: DOCKER_BUILDKIT=0 forces the legacy builder" + fix = "Unset DOCKER_BUILDKIT." + } + var out []Finding + for _, svc := range env.Project.Services { + if svc.Build == nil || svc.Build.Dockerfile == nil { + continue + } + for _, feat := range svc.Build.Dockerfile.BuildKitFeatures { + predicts := "" + if strings.Contains(feat.Name, "--") { + predicts = "the --" + lastWord(feat.Name) + " option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled" + } + out = append(out, Finding{ + Rule: "build.buildkit", + Severity: fact.Error, + Service: svc.Name, + Title: feat.Name + " requires BuildKit", + Evidence: []string{reason}, + Fix: fix, + Location: feat.Location.String(), + Predicts: predicts, + }) + } + } + return out + }, +} + +var composeFeaturesRule = rule{ + id: "compose.version", + title: "Installed Compose understands the Compose file", + needs: []fact.ID{fact.Compose}, + check: func(_ context.Context, env *Env) []Finding { + have := env.Host.ComposeVersion + var out []Finding + if strings.HasPrefix(have, "1.") { + out = append(out, Finding{ + Rule: "compose.version", + Severity: fact.Warning, + Title: "only legacy docker-compose " + have + " is installed", + Evidence: []string{"host: `docker compose` (v2) is missing; found docker-compose " + have}, + Fix: "Install the Compose v2 plugin; v1 has been end-of-life since 2023.", + }) + } + for _, feat := range env.Project.ComposeFeatures { + if versionLess(have, feat.MinVersion) { + out = append(out, Finding{ + Rule: "compose.version", + Severity: fact.Error, + Title: fmt.Sprintf("`%s` needs Compose %s or newer", feat.Name, feat.MinVersion), + Evidence: []string{"host: Compose " + have}, + Fix: "Upgrade Docker Compose (or Docker Desktop).", + Location: feat.Location.String(), + }) + } + } + return out + }, +} + +var envRule = rule{ + id: "compose.env", + title: "Interpolated variables have values", + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + seen := map[string]bool{} + for _, ref := range env.Project.EnvRefs { + if _, set := env.Project.Env[ref.Name]; set || ref.HasDefault || seen[ref.Name] { + continue + } + seen[ref.Name] = true + if ref.Required { + out = append(out, Finding{ + Rule: "compose.env", + Severity: fact.Error, + Title: "required variable " + ref.Name + " is not set", + Evidence: []string{"project: ${" + ref.Name + ":?…} has no value in the environment or .env"}, + Fix: "Set " + ref.Name + " in .env or the shell.", + Location: ref.Location.String(), + Predicts: "required variable " + ref.Name + " is missing a value", + }) + continue + } + out = append(out, Finding{ + Rule: "compose.env", + Severity: fact.Warning, + Title: "variable " + ref.Name + " is not set and defaults to an empty string", + Evidence: []string{"project: $" + ref.Name + " not found in the environment or .env"}, + Fix: "Set it in .env, or write ${" + ref.Name + ":-default}.", + Location: ref.Location.String(), + Predicts: `The "` + ref.Name + `" variable is not set. Defaulting to a blank string.`, + }) + } + for _, ef := range env.Project.EnvFiles { + if _, err := os.Stat(ef.Path); err != nil && !ef.Required { + out = append(out, Finding{ + Rule: "compose.env", + Severity: fact.Info, + Service: ef.Service, + Title: "optional env_file " + rel(env, ef.Path) + " is missing", + Fix: "Create it if the service needs those variables.", + }) + } + } + return out + }, +} + +func rel(env *Env, path string) string { + if r, err := filepath.Rel(env.Project.Dir, path); err == nil && !strings.HasPrefix(r, "..") { + return r + } + return path +} + +func lastWord(s string) string { + s = s[strings.LastIndex(s, " ")+1:] + return strings.TrimPrefix(s, "--") +} + +// versionLess compares dotted versions numerically, ignoring suffixes such +// as "-desktop.1". +func versionLess(a, b string) bool { + pa, pb := versionParts(a), versionParts(b) + for i := 0; i < 3; i++ { + if pa[i] != pb[i] { + return pa[i] < pb[i] + } + } + return false +} + +func versionParts(v string) [3]int { + var out [3]int + v = strings.TrimPrefix(v, "v") + if i := strings.IndexAny(v, "-+ "); i >= 0 { + v = v[:i] + } + for i, p := range strings.SplitN(v, ".", 3) { + out[i], _ = strconv.Atoi(p) + } + return out +} diff --git a/pkg/preflight/rules/host_io.go b/pkg/preflight/rules/host_io.go new file mode 100644 index 0000000..83564ac --- /dev/null +++ b/pkg/preflight/rules/host_io.go @@ -0,0 +1,220 @@ +package rules + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" +) + +type portUse struct { + service string + loc string +} + +var portConflictRule = rule{ + id: "ports.conflict", + title: "Services do not publish the same host port", + needs: []fact.ID{fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + used := map[string]portUse{} + for _, svc := range env.Project.Services { + for _, p := range svc.Ports { + width := p.End - p.Start + 1 + if svc.Replicas > 1 && width < svc.Replicas { + out = append(out, Finding{ + Rule: "ports.conflict", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("%d replicas cannot all publish host port %s", svc.Replicas, p.String()), + Evidence: []string{"project: each replica binds the same host port"}, + Fix: fmt.Sprintf("Publish a range at least %d wide (e.g. \"%d-%d:%d\"), drop the host port, or put a proxy in front.", svc.Replicas, p.Start, p.Start+svc.Replicas-1, p.Target), + Location: svc.Location.String(), + Predicts: "Bind for 0.0.0.0:" + fmt.Sprint(p.Start) + " failed: port is already allocated", + }) + } + for port := p.Start; port <= p.End; port++ { + key := fmt.Sprintf("%s:%d/%s", p.HostIP, port, p.Protocol) + if prev, ok := used[key]; ok && prev.service != svc.Name { + out = append(out, Finding{ + Rule: "ports.conflict", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("host port %d/%s is published by both %s and %s", port, p.Protocol, prev.service, svc.Name), + Fix: "Give one of them a different host port.", + Location: svc.Location.String(), + Predicts: fmt.Sprintf("Bind for 0.0.0.0:%d failed: port is already allocated", port), + }) + break + } + used[key] = portUse{svc.Name, svc.Location.String()} + } + } + } + return out + }, +} + +var hostPortRule = rule{ + id: "ports.host", + title: "Published ports are free on the host", + needs: []fact.ID{fact.LocalDaemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + unprivStart := int64(1024) + if v, ok := h.Sysctls["net.ipv4.ip_unprivileged_port_start"]; ok { + unprivStart = v + } + var out []Finding + busy := map[string]int{} // port -> index in out + for _, svc := range env.Project.Services { + for _, p := range svc.Ports { + if h.Rootless && int64(p.Start) < unprivStart { + out = append(out, Finding{ + Rule: "ports.host", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("rootless Docker cannot publish privileged port %d", p.Start), + Evidence: []string{"host: daemon runs rootless", fmt.Sprintf("host: net.ipv4.ip_unprivileged_port_start = %d", unprivStart)}, + Fix: "Publish a port ≥ 1024, or `sudo sysctl net.ipv4.ip_unprivileged_port_start=0`.", + Location: svc.Location.String(), + Predicts: "cannot expose privileged port " + fmt.Sprint(p.Start), + }) + } + for port := p.Start; port <= p.End; port++ { + key := fmt.Sprintf("%d/%s", port, p.Protocol) + if i, ok := busy[key]; ok { + out[i].Service += ", " + svc.Name + break + } + holder, c := h.PortHolder(p.HostIP, port, p.Protocol) + if holder == "" { + continue + } + // Compose replaces its own containers on `up`. + if c != nil && c.ComposeProject == env.Project.Name { + continue + } + out = append(out, Finding{ + Rule: "ports.host", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("host port %d/%s is already in use by %s", port, p.Protocol, holder), + Evidence: []string{fmt.Sprintf("host: %s is listening on %d", holder, port)}, + Fix: "Stop it, or publish a different host port.", + Location: svc.Location.String(), + Predicts: fmt.Sprintf("Bind for 0.0.0.0:%d failed: port is already allocated / address already in use", port), + }) + busy[key] = len(out) - 1 + break + } + } + } + return out + }, +} + +var bindRule = rule{ + id: "mounts.bind", + title: "Bind-mount sources exist and have the right type", + needs: []fact.ID{fact.LocalDaemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + for _, svc := range env.Project.Services { + for _, b := range svc.Binds { + wantsFile := looksLikeFile(b.Target) + st, err := os.Stat(b.Source) + src := rel(env, b.Source) + switch { + case err != nil && !b.CreateHostPath: + out = append(out, Finding{ + Rule: "mounts.bind", Severity: fact.Error, Service: svc.Name, + Title: "bind source " + src + " does not exist", + Evidence: []string{"project: create_host_path: false"}, + Fix: "Create " + src + " before starting.", + Location: svc.Location.String(), + Predicts: "invalid mount config for type \"bind\": bind source path does not exist", + }) + case err != nil && wantsFile: + out = append(out, Finding{ + Rule: "mounts.bind", Severity: fact.Error, Service: svc.Name, + Title: fmt.Sprintf("bind source %s is missing; Docker will create a directory where %s expects a file", src, b.Target), + Evidence: []string{"host: " + src + " does not exist", "project: target " + b.Target + " looks like a file"}, + Fix: "Create the file " + src + " (check the path is relative to the Compose file).", + Location: svc.Location.String(), + Predicts: "Are you trying to mount a directory onto a file (or vice-versa)?", + }) + case err != nil: + out = append(out, Finding{ + Rule: "mounts.bind", Severity: fact.Warning, Service: svc.Name, + Title: "bind source " + src + " does not exist and will be created empty, owned by root", + Fix: "Create it yourself if the container expects content or write access.", + Location: svc.Location.String(), + }) + case st.IsDir() && wantsFile && isEmptyDir(b.Source): + out = append(out, Finding{ + Rule: "mounts.bind", Severity: fact.Error, Service: svc.Name, + Title: fmt.Sprintf("%s is an empty directory but %s expects a file", src, b.Target), + Evidence: []string{"host: " + src + " is an empty directory, typically left by an earlier run when the file was missing"}, + Fix: "Delete the empty directory " + src + " and create the file.", + Location: svc.Location.String(), + Predicts: "Are you trying to mount a directory onto a file (or vice-versa)?", + }) + } + } + } + return out + }, +} + +// Docker Desktop for Mac shares these host paths with its VM by default. +var desktopSharedPaths = []string{"/Users", "/Volumes", "/private", "/tmp", "/var/folders"} + +var fileSharingRule = rule{ + id: "mounts.sharing", + title: "Bind mounts are inside Docker Desktop's shared paths", + needs: []fact.ID{fact.LocalDaemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + if !h.IsDesktop() || h.ClientOS != "darwin" { + return nil + } + var out []Finding + for _, svc := range env.Project.Services { + for _, b := range svc.Binds { + shared := false + for _, p := range desktopSharedPaths { + if b.Source == p || strings.HasPrefix(b.Source, p+"/") { + shared = true + } + } + if shared { + continue + } + out = append(out, Finding{ + Rule: "mounts.sharing", Severity: fact.Warning, Service: svc.Name, + Title: "bind source " + b.Source + " is outside Docker Desktop's default shared paths", + Evidence: []string{"host: Docker Desktop shares " + strings.Join(desktopSharedPaths, ", ") + " by default"}, + Fix: "Add the path in Docker Desktop → Settings → Resources → File sharing.", + Location: svc.Location.String(), + Predicts: "Mounts denied: The path " + b.Source + " is not shared from the host and is not known to Docker.", + }) + } + } + return out + }, +} + +func looksLikeFile(target string) bool { + base := filepath.Base(target) + return strings.Contains(strings.TrimPrefix(base, "."), ".") && !strings.HasSuffix(target, "/") +} + +func isEmptyDir(path string) bool { + entries, err := os.ReadDir(path) + return err == nil && len(entries) == 0 +} diff --git a/pkg/preflight/rules/images.go b/pkg/preflight/rules/images.go new file mode 100644 index 0000000..8fb1513 --- /dev/null +++ b/pkg/preflight/rules/images.go @@ -0,0 +1,211 @@ +package rules + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +func pullKind(p Pull) string { + if p.Base { + return "base image" + } + return "image" +} + +var imageExistsRule = rule{ + id: "image.exists", + title: "Images exist in their registries", + needs: []fact.ID{fact.Registry, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + reported := map[string]bool{} + for _, p := range Pulls(env.Project, env.Host) { + res, ok := env.Images[ImageKey(p.Ref, p.Platform)] + if p.Local || !ok || res.Err == nil || reported[p.Service+p.Ref] { + continue + } + reported[p.Service+p.Ref] = true + switch { + case errors.Is(res.Err, registry.ErrNotFound): + out = append(out, Finding{ + Rule: "image.exists", + Severity: fact.Error, + Service: p.Service, + Title: fmt.Sprintf("%s %s does not exist", pullKind(p), p.Ref), + Evidence: []string{"registry: " + firstLine(res.Err)}, + Fix: "Check the tag on the registry's web page; tags are often renamed or removed.", + Location: p.Location.String(), + Predicts: "failed to resolve reference \"" + registry.Canonical(p.Ref) + "\": not found", + }) + case errors.Is(res.Err, registry.ErrUnauthorized): + out = append(out, Finding{ + Rule: "image.exists", + Severity: fact.Warning, + Service: p.Service, + Title: fmt.Sprintf("%s %s is private or does not exist", pullKind(p), p.Ref), + Evidence: []string{"registry: " + firstLine(res.Err), "Docker Hub gives the same answer for private and missing repositories"}, + Fix: "Check the name, or `docker login` to the registry that hosts it.", + Location: p.Location.String(), + Predicts: "pull access denied, repository does not exist or may require 'docker login'", + }) + } + } + return out + }, +} + +var imagePlatformRule = rule{ + id: "image.platform", + title: "Images ship the platform this host runs", + needs: []fact.ID{fact.Daemon, fact.Registry, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + var out []Finding + h := env.Host + for _, p := range Pulls(env.Project, h) { + res, ok := env.Images[ImageKey(p.Ref, p.Platform)] + if !ok || res.Err != nil { + continue + } + img := res.Image + want := registry.Normalize(p.Platform) + evidence := []string{ + fmt.Sprintf("host: daemon runs %s (%s)", h.Platform, h.OperatingSystem), + fmt.Sprintf("registry: %s ships %s", p.Ref, strings.Join(img.Platforms, ", ")), + } + if p.Explicit { + evidence = append(evidence, "project: platform "+want+" requested explicitly") + } + + available := false + for _, pl := range img.Platforms { + if registry.Compatible(pl, want) { + available = true + } + } + + if img.Index && !available { + fix := "Use an image or tag that publishes " + want + "." + if !p.Explicit { + for _, pl := range img.Platforms { + if _, emu := h.CanRun(pl); emu { + fix = fmt.Sprintf("Add `platform: %s` to run it under emulation, or use an image that publishes %s.", pl, want) + break + } + } + } + out = append(out, Finding{ + Rule: "image.platform", + Severity: fact.Error, + Service: p.Service, + Title: fmt.Sprintf("%s %s has no %s variant", pullKind(p), p.Ref, want), + Evidence: evidence, + Fix: fix, + Location: p.Location.String(), + Predicts: "no matching manifest for " + want + " in the manifest list entries", + }) + continue + } + + // The image will be fetched for its own platform; it must be + // runnable here. + runAs := want + if !available && len(img.Platforms) == 1 { + runAs = img.Platforms[0] + } + native, emulated := h.CanRun(runAs) + switch { + case native: + case !emulated && !h.EmulationKnown: + out = append(out, Finding{ + Rule: "image.platform", + Severity: fact.Warning, + Service: p.Service, + Title: fmt.Sprintf("%s %s is %s only; could not tell whether this host emulates it", pullKind(p), p.Ref, runAs), + Evidence: append(evidence, "host: the daemon's kernel could not be inspected for binfmt handlers"), + Fix: "Check with `docker run --rm --platform " + runAs + " alpine uname -m`, or use an image built for " + h.Platform + ".", + Location: p.Location.String(), + Predicts: "exec format error (if no emulation)", + }) + case emulated && h.Emulators[registry.Normalize(runAs)] == "qemu" && qemuCrash(p.Ref) != "": + out = append(out, Finding{ + Rule: "image.platform", + Severity: fact.Error, + Service: p.Service, + Title: fmt.Sprintf("%s %s crashes under QEMU %s emulation", pullKind(p), p.Ref, runAs), + Evidence: append(evidence, "host: "+runAs+" is emulated with QEMU (no Rosetta)", "known: this image does not run under QEMU user-mode emulation"), + Fix: "Use a native " + h.Platform + " alternative, enable Rosetta (`colima start --vz-rosetta`, or Docker Desktop's Rosetta setting), or run on an amd64 host.", + Location: p.Location.String(), + Predicts: qemuCrash(p.Ref), + }) + case emulated: + out = append(out, Finding{ + Rule: "image.platform", + Severity: fact.Warning, + Service: p.Service, + Title: fmt.Sprintf("%s %s will run under %s emulation", pullKind(p), p.Ref, runAs), + Evidence: append(evidence, "host: can emulate "+strings.Join(h.Emulated, ", ")), + Fix: "Expect it to be several times slower; JIT runtimes and databases can crash under emulation. Prefer a native " + h.Platform + " image.", + Location: p.Location.String(), + Predicts: "The requested image's platform (" + runAs + ") does not match the detected host platform (" + h.Platform + ")", + }) + default: + out = append(out, Finding{ + Rule: "image.platform", + Severity: fact.Error, + Service: p.Service, + Title: fmt.Sprintf("%s %s is %s only and this host cannot run it", pullKind(p), p.Ref, runAs), + Evidence: append(evidence, "host: no emulation for "+runAs), + Fix: "Install QEMU binfmt handlers (`docker run --privileged --rm tonistiigi/binfmt --install all`) or use an image built for " + h.Platform + ".", + Location: p.Location.String(), + Predicts: "exec format error", + }) + } + } + return out + }, +} + +var credentialsRule = rule{ + id: "registry.credentials", + title: "Docker credential helper works", + needs: []fact.ID{fact.DockerCLI}, + check: func(_ context.Context, env *Env) []Finding { + helper := env.Host.MissingCredentialHelper + if helper == "" { + return nil + } + return []Finding{{ + Rule: "registry.credentials", + Severity: fact.Error, + Title: "Docker's configured credential helper " + helper + " is not installed", + Evidence: []string{"host: ~/.docker/config.json names " + helper + ", which is not on PATH"}, + Fix: "Install the helper, or remove the \"credsStore\" entry from ~/.docker/config.json (common after uninstalling Docker Desktop).", + Predicts: "error getting credentials - err: exec: \"" + helper + "\": executable file not found in $PATH", + }} + }, +} + +func qemuCrash(ref string) string { + for _, k := range knownFor(ref) { + if k.qemuCrash != "" { + return k.qemuCrash + } + } + return "" +} + +func firstLine(err error) string { + s := err.Error() + if i := strings.IndexByte(s, '\n'); i >= 0 { + s = s[:i] + } + if len(s) > 200 { + s = s[:200] + "…" + } + return s +} diff --git a/pkg/preflight/rules/resources.go b/pkg/preflight/rules/resources.go new file mode 100644 index 0000000..aeb9741 --- /dev/null +++ b/pkg/preflight/rules/resources.go @@ -0,0 +1,309 @@ +package rules + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/project" +) + +const mib = 1024 * 1024 + +// knownImage records hard host requirements of popular images that are not +// visible in their manifests. Each entry is tied to the error the image +// prints when the requirement is not met. +type knownImage struct { + match string + minMemory int64 + maxMapCount int64 + + // qemuCrash is the error printed when the image runs under QEMU + // user-mode emulation, which it does not support. + qemuCrash string + + skip func(env map[string]string) bool + predicts string +} + +var knownImages = []knownImage{ + { + match: "mssql/server", + minMemory: 2000 * mib, + qemuCrash: "sqlservr: Invalid mapping of address … in reserved address space below 0x400000000000", + predicts: "sqlservr: This program requires a machine with at least 2000 megabytes of memory.", + }, + { + match: "elasticsearch", + maxMapCount: 262144, + skip: singleNode, + predicts: "max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]", + }, + { + match: "opensearch", + maxMapCount: 262144, + skip: singleNode, + predicts: "max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]", + }, + { + match: "sonarqube", + maxMapCount: 262144, + predicts: "max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]", + }, +} + +// singleNode: Elasticsearch skips its bootstrap checks in single-node mode. +func singleNode(env map[string]string) bool { + return env["discovery.type"] == "single-node" +} + +// imagesOf returns the service image plus the external base images its +// build uses. +func imagesOf(svc *project.Service) []string { + var refs []string + if svc.Image != "" { + refs = append(refs, svc.Image) + } + if svc.Build != nil && svc.Build.Dockerfile != nil { + for _, st := range svc.Build.Dockerfile.Stages { + if st.BaseStage < 0 { + refs = append(refs, st.Base) + } + } + } + return refs +} + +func knownFor(ref string) []knownImage { + var out []knownImage + for _, k := range knownImages { + if strings.Contains(ref, k.match) { + out = append(out, k) + } + } + return out +} + +func known(svc *project.Service) []knownImage { + var out []knownImage + for _, ref := range imagesOf(svc) { + for _, k := range knownImages { + if strings.Contains(ref, k.match) && (k.skip == nil || !k.skip(svc.Environment)) { + out = append(out, k) + } + } + } + return out +} + +var memoryRule = rule{ + id: "resources.memory", + title: "Docker has enough memory for the services", + needs: []fact.ID{fact.Daemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + avail := h.MemTotal + where := fmt.Sprintf("host: Docker daemon has %s of memory", humanBytes(avail)) + if h.IsDesktop() && h.ClientMem > 0 { + where = fmt.Sprintf("host: Docker Desktop VM has %s (this machine has %s; containers only see the VM)", humanBytes(avail), humanBytes(h.ClientMem)) + } + var out []Finding + var total int64 + var parts []string + for _, svc := range env.Project.Services { + need := max(svc.MemReservation, svc.MemLimit) + reason := "limit/reservation" + for _, k := range known(svc) { + if k.minMemory > avail { + fix := "Give Docker more memory." + if h.IsDesktop() { + fix = "Raise Docker Desktop's memory (Settings → Resources) above " + humanBytes(k.minMemory) + "." + } + out = append(out, Finding{ + Rule: "resources.memory", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("%s needs at least %s of memory", k.match, humanBytes(k.minMemory)), + Evidence: []string{where}, + Fix: fix, + Location: svc.Location.String(), + Predicts: k.predicts, + }) + } + if k.minMemory > need { + need, reason = k.minMemory, "known minimum for "+k.match + } + } + if svc.MemLimit > avail { + out = append(out, Finding{ + Rule: "resources.memory", + Severity: fact.Warning, + Service: svc.Name, + Title: fmt.Sprintf("memory limit %s is larger than all memory Docker has", humanBytes(svc.MemLimit)), + Evidence: []string{where}, + Fix: "The limit cannot be reached; the kernel OOM-kills the container first. Lower the limit or give Docker more memory.", + Location: svc.Location.String(), + Predicts: "OOMKilled", + }) + } + if need > 0 { + n := int64(max(svc.Replicas, 1)) + total += need * n + parts = append(parts, fmt.Sprintf("%s %s×%d (%s)", svc.Name, humanBytes(need), n, reason)) + } + } + if total > avail { + sort.Strings(parts) + out = append(out, Finding{ + Rule: "resources.memory", + Severity: fact.Warning, + Title: fmt.Sprintf("services need %s together; Docker has %s", humanBytes(total), humanBytes(avail)), + Evidence: append([]string{where}, "project: "+strings.Join(parts, ", ")), + Fix: "Expect OOM kills under load. Give Docker more memory or run fewer services/replicas.", + Predicts: "OOMKilled / exit code 137", + }) + } + return out + }, +} + +// A registry reports compressed layer sizes; unpacked images are typically +// 2–3× larger. +const unpackFactor = 2.5 + +var diskRule = rule{ + id: "resources.disk", + title: "Enough disk for the images to pull", + needs: []fact.ID{fact.LocalDaemon, fact.Registry, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + if h.DiskFree == 0 { + return nil + } + var compressed int64 + counted := map[string]bool{} + var biggest []string + for _, p := range Pulls(env.Project, h) { + key := ImageKey(p.Ref, p.Platform) + res := env.Images[key] + if p.Local || res.Err != nil || res.Image == nil || counted[key] { + continue + } + counted[key] = true + compressed += res.Image.CompressedSize + if res.Image.CompressedSize > 200*mib { + biggest = append(biggest, fmt.Sprintf("%s %s", p.Ref, humanBytes(res.Image.CompressedSize))) + } + } + need := int64(float64(compressed) * unpackFactor) + where := fmt.Sprintf("host: %s free", humanBytes(h.DiskFree)) + if !h.DiskExact { + where += " on this machine (Docker's VM disk may have less)" + } + evidence := []string{where, fmt.Sprintf("registry: %s to download, ≈%s unpacked", humanBytes(compressed), humanBytes(need))} + if len(biggest) > 0 { + evidence = append(evidence, "largest: "+strings.Join(biggest, ", ")) + } + switch { + case need > h.DiskFree: + return []Finding{{ + Rule: "resources.disk", + Severity: fact.Error, + Title: "not enough disk space to pull the images", + Evidence: evidence, + Fix: "Free space (`docker system prune`) or enlarge Docker's disk.", + Predicts: "no space left on device", + }} + case h.DiskExact && need > h.DiskFree*9/10: + return []Finding{{ + Rule: "resources.disk", + Severity: fact.Warning, + Title: "pulling the images will nearly fill the disk", + Evidence: evidence, + Fix: "Free space with `docker system prune`.", + }} + } + return nil + }, +} + +var gpuRule = rule{ + id: "resources.gpu", + title: "GPU requests can be satisfied", + needs: []fact.ID{fact.Daemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + hasNvidia := false + for _, r := range h.Runtimes { + if strings.Contains(r, "nvidia") { + hasNvidia = true + } + } + var out []Finding + for _, svc := range env.Project.Services { + if !svc.GPU || hasNvidia { + continue + } + evidence := []string{"host: runtimes " + strings.Join(h.Runtimes, ", ") + " (no nvidia)"} + fix := "Install the NVIDIA Container Toolkit and restart Docker." + if h.ClientOS == "darwin" { + evidence = append(evidence, "host: Docker on macOS cannot pass GPUs to containers") + fix = "Run this service on a Linux or Windows host with an NVIDIA GPU, or make the GPU optional." + } + out = append(out, Finding{ + Rule: "resources.gpu", + Severity: fact.Error, + Service: svc.Name, + Title: "service requests a GPU this host cannot provide", + Evidence: evidence, + Fix: fix, + Location: svc.Location.String(), + Predicts: `could not select device driver "" with capabilities: [[gpu]]`, + }) + } + return out + }, +} + +var sysctlRule = rule{ + id: "kernel.sysctl", + title: "Kernel settings meet image requirements", + needs: []fact.ID{fact.LocalDaemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + have, ok := env.Host.Sysctls["vm.max_map_count"] + var out []Finding + for _, svc := range env.Project.Services { + for _, k := range known(svc) { + if k.maxMapCount == 0 || (ok && have >= k.maxMapCount) { + continue + } + if !ok { + out = append(out, Finding{ + Rule: "kernel.sysctl", + Severity: fact.Warning, + Service: svc.Name, + Title: fmt.Sprintf("%s needs vm.max_map_count ≥ %d; could not read it on this host", k.match, k.maxMapCount), + Evidence: []string{"host: the daemon's kernel could not be inspected"}, + Fix: "Check with `sysctl vm.max_map_count` where the daemon runs.", + Location: svc.Location.String(), + Predicts: k.predicts, + }) + continue + } + out = append(out, Finding{ + Rule: "kernel.sysctl", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("%s needs vm.max_map_count ≥ %d", k.match, k.maxMapCount), + Evidence: []string{fmt.Sprintf("host: vm.max_map_count = %d", have)}, + Fix: fmt.Sprintf("sudo sysctl -w vm.max_map_count=%d (persist it in /etc/sysctl.conf), or set discovery.type=single-node for development.", k.maxMapCount), + Location: svc.Location.String(), + Predicts: k.predicts, + }) + } + } + return out + }, +} diff --git a/pkg/preflight/rules/rule.go b/pkg/preflight/rules/rule.go new file mode 100644 index 0000000..ae631d9 --- /dev/null +++ b/pkg/preflight/rules/rule.go @@ -0,0 +1,187 @@ +// Package rules holds the preflight rules. Each rule joins a project +// requirement with a host capability and predicts a concrete failure, +// quoting the evidence from both sides. +package rules + +import ( + "context" + "fmt" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/project" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +// Env is everything a rule may read. +type Env struct { + Project *project.Project + Host *host.Profile + Facts *fact.Set + + // Images holds registry lookups keyed by ImageKey. It is filled + // before rules run. + Images map[string]ImageResult +} + +type ImageResult struct { + Image *registry.Image + Err error +} + +func ImageKey(ref, platform string) string { + return registry.Canonical(ref) + "|" + registry.Normalize(platform) +} + +// Finding is one predicted failure (or risk), with the evidence behind it. +type Finding struct { + Rule string `json:"rule"` + Severity fact.Severity `json:"severity"` + Service string `json:"service,omitempty"` + Title string `json:"title"` + Evidence []string `json:"evidence,omitempty"` + Fix string `json:"fix,omitempty"` + Location string `json:"location,omitempty"` + + // Predicts is the error Docker would print, when known. It links the + // finding to real-world failure reports. + Predicts string `json:"predicts,omitempty"` +} + +// Rule checks one kind of project/host mismatch. +type Rule interface { + ID() string + Title() string + // Needs lists the facts that must be known for the rule to run. + Needs() []fact.ID + Check(ctx context.Context, env *Env) []Finding +} + +type rule struct { + id string + title string + needs []fact.ID + check func(ctx context.Context, env *Env) []Finding +} + +func (r rule) ID() string { return r.id } +func (r rule) Title() string { return r.title } +func (r rule) Needs() []fact.ID { return r.needs } +func (r rule) Check(ctx context.Context, env *Env) []Finding { return r.check(ctx, env) } + +// All returns every rule in report order. +func All() []Rule { + return []Rule{ + imageExistsRule, + imagePlatformRule, + credentialsRule, + buildContextRule, + buildKitRule, + composeFeaturesRule, + envRule, + memoryRule, + diskRule, + gpuRule, + sysctlRule, + portConflictRule, + hostPortRule, + bindRule, + fileSharingRule, + } +} + +// hostPlatform is the daemon's platform, or this machine's when the daemon +// is unknown (they match for local Docker Desktop, Colima and Engine). +func hostPlatform(h *host.Profile) string { + if h.Platform != "" { + return h.Platform + } + return "linux/" + h.ClientArch +} + +// Pull is an image Docker will fetch for a service: either the service image +// or a base image of a build stage that BuildKit will actually build. +type Pull struct { + Service string + Ref string + Platform string + Explicit bool + Base bool + Location project.Location + + // Local is true when the image is already in the daemon, so Compose + // will not pull it. Its platform still matters. + Local bool +} + +// Pulls lists every image the project uses, for the platform Docker will +// request. +func Pulls(p *project.Project, h *host.Profile) []Pull { + var out []Pull + seen := map[string]bool{} + add := func(pl Pull) { + pl.Local = h.LocalImages[registry.Canonical(pl.Ref)] + key := pl.Service + "|" + ImageKey(pl.Ref, pl.Platform) + if !seen[key] { + seen[key] = true + out = append(out, pl) + } + } + native := hostPlatform(h) + + for _, svc := range p.Services { + targets := []string{native} + explicit := false + if svc.Platform != "" { + targets, explicit = []string{svc.Platform}, true + } else if svc.Build != nil && len(svc.Build.Platforms) > 0 { + targets, explicit = svc.Build.Platforms, true + } + + if svc.Image != "" { + for _, t := range targets { + add(Pull{Service: svc.Name, Ref: svc.Image, Platform: t, Explicit: explicit, Location: svc.Location}) + } + } + if svc.Build == nil || svc.Build.Dockerfile == nil { + continue + } + df := svc.Build.Dockerfile + reach, err := df.Reachable(svc.Build.Target) + if err != nil { + continue + } + for _, i := range reach { + st := df.Stages[i] + if st.BaseStage >= 0 || strings.EqualFold(st.Base, "scratch") || st.Unresolved() { + continue + } + for _, t := range targets { + pl, exp := t, explicit + switch { + case strings.Contains(st.Platform, "BUILDPLATFORM"): + pl, exp = native, true + case strings.Contains(st.Platform, "TARGETPLATFORM"): + case st.Platform != "" && !strings.Contains(st.Platform, "$"): + pl, exp = st.Platform, true + } + add(Pull{Service: svc.Name, Ref: st.Base, Platform: pl, Explicit: exp, Base: true, Location: st.Location}) + } + } + } + return out +} + +func humanBytes(n int64) string { + const unit = 1024 + if n < unit { + return fmt.Sprintf("%d B", n) + } + div, exp := int64(unit), 0 + for m := n / unit; m >= unit; m /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp]) +} diff --git a/pkg/preflight/rules/rules_test.go b/pkg/preflight/rules/rules_test.go new file mode 100644 index 0000000..99b3588 --- /dev/null +++ b/pkg/preflight/rules/rules_test.go @@ -0,0 +1,399 @@ +package rules + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/project" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +func load(t *testing.T, files map[string]string) *project.Project { + t.Helper() + dir := t.TempDir() + for name, content := range files { + p := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + facts := fact.NewSet() + p, err := project.Load(context.Background(), dir, facts) + if err != nil { + t.Fatal(err) + } + if r, ok := facts.Root(fact.ComposeFile); ok { + t.Fatalf("compose load failed: %s", r.Detail) + } + return p +} + +// armDesktop is a MacBook running Docker Desktop with 4 GiB for the VM. +func armDesktop() *host.Profile { + return &host.Profile{ + ClientOS: "darwin", ClientArch: "arm64", ClientMem: 16 << 30, + Platform: "linux/arm64", OperatingSystem: "Docker Desktop", + MemTotal: 4 << 30, Runtimes: []string{"runc"}, + ComposeVersion: "2.29.1", BuildxInstalled: true, + Emulated: []string{"linux/amd64"}, EmulationKnown: true, + Sysctls: map[string]int64{"vm.max_map_count": 262144}, + Local: true, DiskFree: 50 << 30, + LocalImages: map[string]bool{}, PublishedPorts: map[string][]host.Container{}, + } +} + +// oldLinux is an amd64 server with distro packages and default sysctls. +func oldLinux() *host.Profile { + return &host.Profile{ + ClientOS: "linux", ClientArch: "amd64", + Platform: "linux/amd64", OperatingSystem: "Ubuntu 20.04", + MemTotal: 8 << 30, Runtimes: []string{"runc"}, + ComposeVersion: "1.29.2", EmulationKnown: true, + Sysctls: map[string]int64{"vm.max_map_count": 65530}, + Local: true, DiskFree: 2 << 30, DiskExact: true, + LocalImages: map[string]bool{}, PublishedPorts: map[string][]host.Container{}, + } +} + +// fakeRegistry answers from a table: ref -> platforms (prefix "single:" for a +// single-platform image), or an error. +type fakeRegistry map[string]string + +func (f fakeRegistry) Resolve(_ context.Context, ref, platform string) (*registry.Image, error) { + v, ok := f[ref] + if !ok { + return nil, fmt.Errorf("%w: %s", registry.ErrNotFound, ref) + } + if v == "401" { + return nil, fmt.Errorf("%w: %s", registry.ErrUnauthorized, ref) + } + img := ®istry.Image{Ref: ref, Index: true, CompressedSize: 500 << 20} + if s, ok := strings.CutPrefix(v, "single:"); ok { + img.Index, v = false, s + } + img.Platforms = strings.Split(v, ",") + return img, nil +} + +func run(t *testing.T, r Rule, p *project.Project, h *host.Profile, reg fakeRegistry) []Finding { + t.Helper() + env := &Env{Project: p, Host: h, Facts: fact.NewSet(), Images: map[string]ImageResult{}} + for _, pl := range Pulls(p, h) { + img, err := reg.Resolve(context.Background(), pl.Ref, pl.Platform) + env.Images[ImageKey(pl.Ref, pl.Platform)] = ImageResult{img, err} + } + return r.Check(context.Background(), env) +} + +func titles(fs []Finding) string { + var out []string + for _, f := range fs { + out = append(out, string(f.Severity)+": "+f.Title) + } + return strings.Join(out, "\n") +} + +func expect(t *testing.T, fs []Finding, sev fact.Severity, substr string) { + t.Helper() + for _, f := range fs { + if f.Severity == sev && strings.Contains(f.Title, substr) { + return + } + } + t.Errorf("want %s finding containing %q, got:\n%s", sev, substr, titles(fs)) +} + +func expectNone(t *testing.T, fs []Finding) { + t.Helper() + if len(fs) > 0 { + t.Errorf("want no findings, got:\n%s", titles(fs)) + } +} + +var multiArch = "linux/amd64,linux/arm64" + +func TestImagePlatform(t *testing.T) { + p := load(t, map[string]string{"compose.yaml": `services: + db: {image: "mcr.microsoft.com/mssql/server:2022-latest"} + legacy: {image: "acme/amd64-index:1"} + pinned: {image: "acme/amd64-index:1", platform: linux/amd64} + ok: {image: "postgres:16"} +`}) + reg := fakeRegistry{ + "mcr.microsoft.com/mssql/server:2022-latest": "single:linux/amd64", + "acme/amd64-index:1": "linux/amd64", + "postgres:16": multiArch, + } + + fs := run(t, imagePlatformRule, p, armDesktop(), reg) + expect(t, fs, fact.Warning, "mssql/server:2022-latest will run under linux/amd64 emulation") + expect(t, fs, fact.Error, "acme/amd64-index:1 has no linux/arm64 variant") + expect(t, fs, fact.Warning, "acme/amd64-index:1 will run under linux/amd64 emulation") // pinned + if len(fs) != 3 { + t.Errorf("postgres should pass; got:\n%s", titles(fs)) + } + + noEmu := armDesktop() + noEmu.OperatingSystem, noEmu.Emulated = "Ubuntu 24.04", nil + fs = run(t, imagePlatformRule, p, noEmu, reg) + expect(t, fs, fact.Error, "is linux/amd64 only and this host cannot run it") + + colima := armDesktop() + colima.OperatingSystem, colima.Emulators = "Ubuntu 24.04", map[string]string{"linux/amd64": "qemu"} + fs = run(t, imagePlatformRule, p, colima, reg) + expect(t, fs, fact.Error, "mssql/server:2022-latest crashes under QEMU linux/amd64 emulation") + + rosetta := armDesktop() + rosetta.Emulators = map[string]string{"linux/amd64": "rosetta"} + expect(t, run(t, imagePlatformRule, p, rosetta, reg), fact.Warning, "mssql/server:2022-latest will run under linux/amd64 emulation") + + unknown := armDesktop() + unknown.OperatingSystem, unknown.Emulated, unknown.EmulationKnown = "Ubuntu 24.04", nil, false + fs = run(t, imagePlatformRule, p, unknown, reg) + expect(t, fs, fact.Warning, "could not tell whether this host emulates it") + if strings.Contains(titles(fs), "cannot run it") { + t.Error("unknown emulation must not be reported as a certain failure") + } + + expectNone(t, run(t, imagePlatformRule, p, oldLinux(), reg)) +} + +func TestBaseImagesOfBuildsAreChecked(t *testing.T) { + p := load(t, map[string]string{ + "compose.yaml": "services:\n app: {build: .}\n", + "Dockerfile": `FROM --platform=$BUILDPLATFORM golang:1.23 AS build +FROM acme/amd64-index:1 AS unused +FROM acme/runtime:1 +`, + }) + reg := fakeRegistry{"golang:1.23": multiArch, "acme/runtime:1": "linux/amd64", "acme/amd64-index:1": "linux/amd64"} + fs := run(t, imagePlatformRule, p, armDesktop(), reg) + expect(t, fs, fact.Error, "base image acme/runtime:1 has no linux/arm64 variant") + if strings.Contains(titles(fs), "amd64-index") { + t.Error("unreachable stage must not be checked") + } +} + +func TestImageExists(t *testing.T) { + p := load(t, map[string]string{"compose.yaml": `services: + a: {image: "redis:7.9.99"} + b: {image: "acme/private:1"} + c: {image: "redis:7"} +`}) + h := armDesktop() + h.LocalImages[registry.Canonical("redis:7")] = true + fs := run(t, imageExistsRule, p, h, fakeRegistry{"acme/private:1": "401"}) + expect(t, fs, fact.Error, "redis:7.9.99 does not exist") + expect(t, fs, fact.Warning, "acme/private:1 is private or does not exist") + if len(fs) != 2 { + t.Errorf("local image must not be looked up:\n%s", titles(fs)) + } +} + +func TestBuildContext(t *testing.T) { + p := load(t, map[string]string{ + "compose.yaml": `services: + web: {build: ./web} + api: {build: {context: ./api, target: nope}} + gone: {build: ./missing} +`, + "web/Dockerfile": `FROM node:20 AS build +COPY package.json yarn.lock ./ +COPY .env ./ +COPY ../shared ./shared +FROM nginx +COPY --from=build /app /app +COPY dist/ /usr/share/nginx/html +`, + "web/package.json": "{}", + "web/.env": "", + "web/.dockerignore": ".env\n", + "api/Dockerfile": "FROM alpine AS base\n", + }) + fs := run(t, buildContextRule, p, armDesktop(), nil) + expect(t, fs, fact.Error, `COPY source "yarn.lock" not found`) + expect(t, fs, fact.Error, `COPY source ".env" is excluded by .dockerignore`) + expect(t, fs, fact.Error, `"../shared" is outside the build context`) + expect(t, fs, fact.Error, `COPY source "dist/" not found`) + expect(t, fs, fact.Error, `build target "nope" is not a stage`) + expect(t, fs, fact.Error, "build context missing does not exist") + if strings.Contains(titles(fs), "package.json") { + t.Error("existing file flagged") + } +} + +func TestBuildKit(t *testing.T) { + p := load(t, map[string]string{ + "compose.yaml": "services:\n app: {build: .}\n", + "Dockerfile": "FROM alpine\nRUN --mount=type=cache,target=/c true\nCOPY --chmod=755 x /x\nRUN < Date: Wed, 23 Sep 2026 22:14:46 +0100 Subject: [PATCH 02/23] feat(preflight): detect low host disk for VM-based Docker engines A VM engine's disk image is a sparse file on the host volume, so the disk estimate now uses the smaller of VM and host free space, and a new host.disk rule warns below 10 GiB and errors below 2 GiB. When the daemon is down on a nearly full disk, the diagnosis says to free space before starting the VM. --- README.md | 3 +- pkg/preflight/host/diagnose.go | 12 ++++++-- pkg/preflight/host/diagnose_test.go | 12 ++++++++ pkg/preflight/host/host.go | 17 ++++++++++- pkg/preflight/rules/resources.go | 46 ++++++++++++++++++++++++++--- pkg/preflight/rules/rule.go | 1 + pkg/preflight/rules/rules_test.go | 26 ++++++++++++++++ 7 files changed, 109 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b820db1..4bc2e7a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ container-doctor preflight examples/broken-shop --host examples/hosts/ubuntu-leg | `compose.version` | Compose features newer than the installed Compose | | `compose.env` | unset variables, required `${VAR:?}` without a value | | `resources.memory` | known image minimums and total limits vs Docker's memory | -| `resources.disk` | image download size ×2.5 vs free disk | +| `resources.disk` | image download size ×2.5 vs free disk (the smaller of the VM disk and the host volume it lives on) | +| `host.disk` | too little space on this machine for Docker Desktop/Colima/OrbStack to start or grow their VM | | `resources.gpu` | GPU requests without the NVIDIA runtime, or on macOS | | `kernel.sysctl` | `vm.max_map_count` for Elasticsearch/OpenSearch/SonarQube | | `ports.conflict` | two services, or several replicas, on one host port | diff --git a/pkg/preflight/host/diagnose.go b/pkg/preflight/host/diagnose.go index 4edd4d7..ca33a8a 100644 --- a/pkg/preflight/host/diagnose.go +++ b/pkg/preflight/host/diagnose.go @@ -27,6 +27,10 @@ type daemonClues struct { Err string Installed []engine Running []engine + + // DiskFree is free space on this machine; VM engines cannot start + // without room for their disk image. + DiskFree int64 } // diagnoseDaemon turns "cannot connect" into the specific reason: the @@ -85,6 +89,10 @@ func diagnoseDaemon(c daemonClues) fact.Failure { if e.Context != "" && e.Context != ctxName { f.Fix += fmt.Sprintf(", then `docker context use %s`", e.Context) } + if c.DiskFree > 0 && c.DiskFree < 2<<30 && e.Name != "Docker Engine" { + f.Fix = fmt.Sprintf("Free disk space first: only %d MiB free, and %s needs a few GiB for its VM. Then: %s", + c.DiskFree>>20, e.Name, strings.TrimPrefix(f.Fix, "Start "+e.Name+": ")) + } if len(c.Installed) > 1 { var others []string for _, o := range c.Installed[1:] { @@ -112,8 +120,8 @@ func socketOwner(socket string) string { // gatherClues looks for local engines: installed apps and binaries, and the // sockets of those that are running. -func gatherClues(ctxName, endpoint, errText string) daemonClues { - c := daemonClues{Context: ctxName, Endpoint: endpoint, Err: errText} +func gatherClues(ctxName, endpoint, errText string, diskFree int64) daemonClues { + c := daemonClues{Context: ctxName, Endpoint: endpoint, Err: errText, DiskFree: diskFree} home, _ := os.UserHomeDir() candidates := []struct { diff --git a/pkg/preflight/host/diagnose_test.go b/pkg/preflight/host/diagnose_test.go index a5b85d3..46fc0ff 100644 --- a/pkg/preflight/host/diagnose_test.go +++ b/pkg/preflight/host/diagnose_test.go @@ -61,3 +61,15 @@ func TestDiagnoseInstalledButStopped(t *testing.T) { t.Errorf("fix = %q", f.Fix) } } + +func TestDiagnoseFullDiskBlocksVMStart(t *testing.T) { + f := diagnoseDaemon(daemonClues{ + Context: "desktop-linux", + Endpoint: "unix:///nonexistent/.docker/run/docker.sock", + Installed: []engine{colima}, + DiskFree: 118 << 20, + }) + if !strings.HasPrefix(f.Fix, "Free disk space first: only 118 MiB free") || !strings.Contains(f.Fix, "colima start") { + t.Errorf("fix = %q", f.Fix) + } +} diff --git a/pkg/preflight/host/host.go b/pkg/preflight/host/host.go index 3e78935..d8474f5 100644 --- a/pkg/preflight/host/host.go +++ b/pkg/preflight/host/host.go @@ -32,6 +32,10 @@ type Profile struct { ClientArch string ClientMem int64 + // ClientDiskFree is free space on this machine's home volume, where + // Docker Desktop, Colima and OrbStack keep their VM disks. + ClientDiskFree int64 + // Platform is the daemon's native platform, e.g. linux/arm64. Platform string OperatingSystem string @@ -93,6 +97,12 @@ type Container struct { ComposeProject string } +// UsesVM reports whether the daemon runs in a VM whose disk image lives on +// this machine and grows as images are pulled. +func (p *Profile) UsesVM() bool { + return p.Local && (p.ClientOS == "darwin" || p.ClientOS == "windows" || p.IsDesktop() || strings.Contains(p.Endpoint, "/.colima/")) +} + // IsDesktop reports whether the daemon runs inside Docker Desktop's VM. func (p *Profile) IsDesktop() bool { return strings.Contains(p.OperatingSystem, "Docker Desktop") @@ -141,6 +151,11 @@ func Probe(ctx context.Context, run executor.Runner, facts *fact.Set) *Profile { if vm, err := mem.VirtualMemoryWithContext(ctx); err == nil { p.ClientMem = int64(vm.Total) } + if home, err := os.UserHomeDir(); err == nil { + if u, err := disk.UsageWithContext(ctx, home); err == nil { + p.ClientDiskFree = int64(u.Free) + } + } p.BuildKitDisabled = os.Getenv("DOCKER_BUILDKIT") == "0" if _, err := run.Run(ctx, "docker", "--version"); err != nil { @@ -176,7 +191,7 @@ func (p *Profile) probeDaemon(ctx context.Context, run executor.Runner, facts *f } if err != nil { ctxName, _ := run.Run(ctx, "docker", "context", "show") - facts.Fail(diagnoseDaemon(gatherClues(ctxName, p.Endpoint, errText(err)))) + facts.Fail(diagnoseDaemon(gatherClues(ctxName, p.Endpoint, errText(err), p.ClientDiskFree))) return } diff --git a/pkg/preflight/rules/resources.go b/pkg/preflight/rules/resources.go index aeb9741..f2bba76 100644 --- a/pkg/preflight/rules/resources.go +++ b/pkg/preflight/rules/resources.go @@ -173,6 +173,37 @@ var memoryRule = rule{ // 2–3× larger. const unpackFactor = 2.5 +// VM engines need room on the host to boot and to grow their disk image. +const ( + vmMinFree = 2 << 30 + vmWarnFree = 10 << 30 +) + +var hostDiskRule = rule{ + id: "host.disk", + title: "This machine has room for Docker's VM", + needs: []fact.ID{fact.LocalDaemon}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + if !h.UsesVM() || h.ClientDiskFree == 0 || h.ClientDiskFree >= vmWarnFree { + return nil + } + f := Finding{ + Rule: "host.disk", + Severity: fact.Warning, + Title: fmt.Sprintf("only %s free on this machine; Docker's VM disk grows here", humanBytes(h.ClientDiskFree)), + Evidence: []string{"host: " + humanBytes(h.ClientDiskFree) + " free on the home volume"}, + Fix: "Free disk space (caches, old simulators, `docker system prune`). Keep at least 10 GiB free.", + } + if h.ClientDiskFree < vmMinFree { + f.Severity = fact.Error + f.Title = fmt.Sprintf("only %s free on this machine; Docker's VM cannot start or grow", humanBytes(h.ClientDiskFree)) + f.Predicts = "no space left on device / VM fails to start" + } + return []Finding{f} + }, +} + var diskRule = rule{ id: "resources.disk", title: "Enough disk for the images to pull", @@ -198,8 +229,15 @@ var diskRule = rule{ } } need := int64(float64(compressed) * unpackFactor) - where := fmt.Sprintf("host: %s free", humanBytes(h.DiskFree)) - if !h.DiskExact { + free := h.DiskFree + where := fmt.Sprintf("host: %s free", humanBytes(free)) + // A VM disk is a sparse file on this machine: it can only grow as + // far as the host volume allows. + if h.UsesVM() && h.ClientDiskFree > 0 && h.ClientDiskFree < free { + free = h.ClientDiskFree + where = fmt.Sprintf("host: VM disk has %s free, but this machine only %s", humanBytes(h.DiskFree), humanBytes(free)) + } + if !h.DiskExact && free == h.DiskFree { where += " on this machine (Docker's VM disk may have less)" } evidence := []string{where, fmt.Sprintf("registry: %s to download, ≈%s unpacked", humanBytes(compressed), humanBytes(need))} @@ -207,7 +245,7 @@ var diskRule = rule{ evidence = append(evidence, "largest: "+strings.Join(biggest, ", ")) } switch { - case need > h.DiskFree: + case need > free: return []Finding{{ Rule: "resources.disk", Severity: fact.Error, @@ -216,7 +254,7 @@ var diskRule = rule{ Fix: "Free space (`docker system prune`) or enlarge Docker's disk.", Predicts: "no space left on device", }} - case h.DiskExact && need > h.DiskFree*9/10: + case h.DiskExact && need > free*9/10: return []Finding{{ Rule: "resources.disk", Severity: fact.Warning, diff --git a/pkg/preflight/rules/rule.go b/pkg/preflight/rules/rule.go index ae631d9..da2786e 100644 --- a/pkg/preflight/rules/rule.go +++ b/pkg/preflight/rules/rule.go @@ -82,6 +82,7 @@ func All() []Rule { envRule, memoryRule, diskRule, + hostDiskRule, gpuRule, sysctlRule, portConflictRule, diff --git a/pkg/preflight/rules/rules_test.go b/pkg/preflight/rules/rules_test.go index 99b3588..4edf233 100644 --- a/pkg/preflight/rules/rules_test.go +++ b/pkg/preflight/rules/rules_test.go @@ -397,3 +397,29 @@ func TestEnv(t *testing.T) { t.Errorf("want exactly 2 findings:\n%s", titles(fs)) } } + +func TestHostDisk(t *testing.T) { + p := load(t, map[string]string{"compose.yaml": "services:\n a: {image: alpine}\n"}) + h := armDesktop() + h.ClientDiskFree = 118 << 20 + expect(t, run(t, hostDiskRule, p, h, nil), fact.Error, "Docker's VM cannot start or grow") + h.ClientDiskFree = 5 << 30 + expect(t, run(t, hostDiskRule, p, h, nil), fact.Warning, "only 5.0 GiB free") + h.ClientDiskFree = 50 << 30 + expectNone(t, run(t, hostDiskRule, p, h, nil)) + + linux := oldLinux() + linux.ClientDiskFree = 100 << 20 + expectNone(t, run(t, hostDiskRule, p, linux, nil)) +} + +func TestDiskUsesSmallerOfVMAndHost(t *testing.T) { + p := load(t, map[string]string{"compose.yaml": "services:\n a: {image: big}\n"}) + h := armDesktop() + h.DiskFree, h.DiskExact, h.ClientDiskFree = 90<<30, true, 1<<30 + fs := run(t, diskRule, p, h, fakeRegistry{"big": multiArch}) + expect(t, fs, fact.Error, "not enough disk space") + if !strings.Contains(strings.Join(fs[0].Evidence, " "), "this machine only") { + t.Errorf("evidence = %v", fs[0].Evidence) + } +} From 39f0d2cfd770b7032d575e8729608eadd90b0935 Mon Sep 17 00:00:00 2001 From: raphgm Date: Wed, 23 Sep 2026 22:28:31 +0100 Subject: [PATCH 03/23] feat: learn preflight rules from real failures learn runs a command (or reads a log), extracts the decisive error line as a generalized signature, and records the service and host features at the time as a rule in .container-doctor/learned.yaml. A repeat failure drops conditions the runs disagree on; a successful run that satisfies a rule adds the feature that distinguishes it, or marks the rule unreliable. Failures a built-in rule already predicts are not learned twice. Preflight evaluates active learned rules. --- README.md | 18 + cmd/learn.go | 247 +++++++++++++ docs/validation/2026-09-23-colima-m-series.md | 17 + pkg/preflight/learn/learn_test.go | 115 ++++++ pkg/preflight/learn/signature.go | 150 ++++++++ pkg/preflight/learn/store.go | 338 ++++++++++++++++++ pkg/preflight/preflight.go | 41 ++- pkg/preflight/rules/features.go | 154 ++++++++ 8 files changed, 1070 insertions(+), 10 deletions(-) create mode 100644 cmd/learn.go create mode 100644 pkg/preflight/learn/learn_test.go create mode 100644 pkg/preflight/learn/signature.go create mode 100644 pkg/preflight/learn/store.go create mode 100644 pkg/preflight/rules/features.go diff --git a/README.md b/README.md index 4bc2e7a..64e0f66 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,24 @@ container-doctor preflight examples/broken-shop --host examples/hosts/macbook-m2 container-doctor preflight examples/broken-shop --host examples/hosts/ubuntu-legacy-server.json ``` +### Learning from real failures + +Built-in rules cover common failures. `learn` turns the failures your project actually hits into new rules: + +```bash +container-doctor learn -- docker compose up db +``` + +It runs the command and extracts the decisive error line from the output. It turns that line into a signature, with addresses, IDs, paths and numbers generalized. It then records the conditions at the time: image, platform, emulation (QEMU/Rosetta), memory, builder, and which environment variables were set (never their values). The rule goes to `.container-doctor/learned.yaml`; commit it, and preflight predicts the failure for everyone on the team. + +Rules improve with evidence: + +- **Another failure with the same error** drops the conditions the two runs disagree on, which makes the rule more general. +- **A working run** (`learn --success -- docker compose up -d`) that satisfies a rule adds the condition that tells it apart from the failures, which makes the rule more specific. If no such condition exists, the rule is marked unreliable and no longer reported. +- **A failure a built-in rule already predicted** is reported as such and is not learned twice. + +Example from a real run: Postgres started without a password failed. `learn` created a rule for `image=postgres`. After a password was added, the successful run refined the rule to `env.POSTGRES_PASSWORD=unset`, and preflight now flags any Compose file that uses Postgres without one. + ### Rules | Rule | Predicts | diff --git a/cmd/learn.go b/cmd/learn.go new file mode 100644 index 0000000..2255549 --- /dev/null +++ b/cmd/learn.go @@ -0,0 +1,247 @@ +package cmd + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + osexec "os/exec" + "regexp" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/raphgm/container-doctor/pkg/preflight" + "github.com/raphgm/container-doctor/pkg/preflight/learn" + "github.com/raphgm/container-doctor/pkg/preflight/project" + "github.com/raphgm/container-doctor/pkg/preflight/rules" +) + +var ( + learnDir string + learnService string + learnFix string + learnLog string + learnSuccess bool + learnForce bool +) + +var learnCmd = &cobra.Command{ + Use: "learn [flags] -- ", + Short: "Turn a real failure into a preflight rule", + Long: `Learn runs a command (usually ` + "`docker compose up`" + `), and if it fails, extracts +the decisive error line and records the project and host conditions at the +time as a rule in ` + learn.File + `. Preflight then predicts that failure +wherever the same conditions hold. Commit the file to share it. + +A second failure with the same error generalizes the rule (conditions that +differ are dropped). Recording a working run with --success refines rules it +contradicts. + + container-doctor learn -- docker compose up db + container-doctor learn --log build.log --service web + container-doctor learn --success -- docker compose up -d`, + RunE: func(cmd *cobra.Command, args []string) error { + output, failed, err := learnInput(args) + if err != nil { + return err + } + + env, err := preflight.Prepare(cmd.Context(), preflight.Options{Dir: learnDir}) + if err != nil { + return err + } + store, err := learn.Load(env.Project.Dir) + if err != nil { + return err + } + hostName, _ := os.Hostname() + + if !failed || learnSuccess { + return recordSuccess(env, store, hostName) + } + + f, ok := learn.Extract(output) + if !ok { + return errors.New("the command failed, but no line in its output explains why; pass the log with --log and --service") + } + if learnService != "" { + f.Service = learnService + } + svc := findService(env.Project, f.Service) + if svc == nil { + return fmt.Errorf("cannot tell which service failed (saw %q); pass --service", f.Service) + } + + fmt.Printf("\nFailure in %s: %s\n", svc.Name, f.Line) + + if !learnForce { + if rule, ok := alreadyPredicted(cmd, env, svc.Name, f.Line); ok { + fmt.Printf("✓ Already predicted by built-in rule %s. Nothing new to learn.\n", rule) + return nil + } + } + + out := store.ObserveFailure(f, rules.Features(env, svc), hostName) + if learnFix != "" { + out.Rule.Fix = learnFix + } + if err := store.Save(); err != nil { + return err + } + + if out.Created { + fmt.Printf("✓ Learned new rule %s\n", out.Rule.ID) + } else { + fmt.Printf("✓ Rule %s seen again (%d failures)\n", out.Rule.ID, len(out.Rule.Failures)) + if len(out.Dropped) > 0 { + fmt.Printf(" generalized: dropped %s\n", strings.Join(out.Dropped, ", ")) + } + } + fmt.Printf(" signature: %s\n", out.Rule.Signature) + fmt.Printf(" predicts it when: %s\n", conditions(out.Rule.When)) + fmt.Printf(" saved to %s — commit it to share with your team\n\n", store.Path()) + return nil + }, +} + +func init() { + rootCmd.AddCommand(learnCmd) + learnCmd.Flags().StringVarP(&learnDir, "dir", "C", ".", "Project directory") + learnCmd.Flags().StringVarP(&learnService, "service", "s", "", "Service that failed (detected from Compose output when omitted)") + learnCmd.Flags().StringVar(&learnFix, "fix", "", "Fix to show when the rule predicts this failure") + learnCmd.Flags().StringVar(&learnLog, "log", "", "Learn from a saved log file (- for stdin) instead of running a command") + learnCmd.Flags().BoolVar(&learnSuccess, "success", false, "Record a working run to refine learned rules") + learnCmd.Flags().BoolVar(&learnForce, "force", false, "Learn even if a built-in rule already predicts the failure") +} + +// learnInput runs the command (echoing its output) or reads the log. +func learnInput(args []string) (string, bool, error) { + if learnLog != "" { + var r io.Reader = os.Stdin + if learnLog != "-" { + f, err := os.Open(learnLog) + if err != nil { + return "", false, err + } + defer f.Close() + r = f + } + b, err := io.ReadAll(r) + return string(b), true, err + } + if len(args) == 0 { + if learnSuccess { + return "", false, nil + } + return "", false, errors.New("give a command after --, or --log") + } + + var buf bytes.Buffer + c := osexec.Command(args[0], args[1:]...) + c.Dir = learnDir + c.Stdin = os.Stdin + c.Stdout = io.MultiWriter(os.Stdout, &buf) + c.Stderr = io.MultiWriter(os.Stderr, &buf) + err := c.Run() + var exit *osexec.ExitError + if err != nil && !errors.As(err, &exit) { + return "", false, err + } + // `docker compose up` exits 0 even when a container crashes. + failed := err != nil || containerExited.MatchString(buf.String()) + return buf.String(), failed, nil +} + +var containerExited = regexp.MustCompile(`exited with code [1-9]\d*`) + +func recordSuccess(env *rules.Env, store *learn.Store, hostName string) error { + targets := env.Project.Services + if learnService != "" { + svc := findService(env.Project, learnService) + if svc == nil { + return fmt.Errorf("no service %q", learnService) + } + targets = []*project.Service{svc} + } + var touched []*learn.Learned + for _, svc := range targets { + touched = append(touched, store.ObserveSuccess(svc.Name, rules.Features(env, svc), hostName)...) + } + if len(touched) == 0 { + fmt.Println("\n✓ Run succeeded; no learned rule predicted a failure here.") + return nil + } + if err := store.Save(); err != nil { + return err + } + fmt.Println("\n✓ Run succeeded where learned rules predicted failure; refined:") + for _, r := range touched { + fmt.Printf(" %s → %s (when: %s)\n", r.ID, r.Status, conditions(r.When)) + } + return nil +} + +func findService(p *project.Project, name string) *project.Service { + for _, s := range p.Services { + if s.Name == name { + return s + } + } + if name == "" && len(p.Services) == 1 { + return p.Services[0] + } + return nil +} + +// alreadyPredicted reports a built-in rule whose predicted error is the +// failure that happened. +func alreadyPredicted(cmd *cobra.Command, env *rules.Env, service, line string) (string, bool) { + rep := preflight.Evaluate(cmd.Context(), env, rules.All(), nil, time.Now()) + for _, f := range rep.Findings { + if f.Predicts == "" || (f.Service != "" && f.Service != service) { + continue + } + if similar(f.Predicts, line) { + return f.Rule, true + } + } + return "", false +} + +var word = regexp.MustCompile(`[a-zA-Z]{3,}`) + +// similar is true when most words of the predicted message occur in the +// observed line. +func similar(predicted, observed string) bool { + have := map[string]bool{} + for _, w := range word.FindAllString(strings.ToLower(observed), -1) { + have[w] = true + } + words := word.FindAllString(strings.ToLower(predicted), -1) + if len(words) == 0 { + return false + } + hit := 0 + for _, w := range words { + if have[w] { + hit++ + } + } + return hit*10 >= len(words)*6 +} + +func conditions(when map[string]string) string { + if len(when) == 0 { + return "(always — record a --success run to refine)" + } + var out []string + for k, v := range when { + out = append(out, k+"="+v) + } + sort.Strings(out) + return strings.Join(out, ", ") +} diff --git a/docs/validation/2026-09-23-colima-m-series.md b/docs/validation/2026-09-23-colima-m-series.md index 15d1c68..316b21a 100644 --- a/docs/validation/2026-09-23-colima-m-series.md +++ b/docs/validation/2026-09-23-colima-m-series.md @@ -48,3 +48,20 @@ Following the fix started a working daemon. The first `colima start` failed beca - Of 11 predictions, 8 identified the real problem (6 word-for-word, 2 with different wording), 1 was a false positive, and 2 were imprecise. All three were caused by facts inside the VM that the first version could not see; reading the VM kernel fixed them. + +## Learning from failures (`learn`) + +1. `mongo:7` forced to `linux/amd64` under QEMU was expected to crash for lack of AVX. It ran, + because this QEMU emulates AVX. Preflight had only warned about emulation, which was the + right severity. +2. `postgres:16-alpine` without `POSTGRES_PASSWORD`. Preflight predicted nothing, because no + rule knew this failure. + - `learn -- docker-compose up` extracted + `Error: Database is uninitialized and superuser password is not specified.` and created + `learned.postgres` with conditions `image=postgres, memory=<2GiB`. + - After a password was added, `learn --success -- docker-compose up -d` observed the working + run. It added the distinguishing condition `env.POSTGRES_PASSWORD=unset`. + - Preflight then reported nothing when the password was set. It predicted the failure when + the password was removed on another tag (`postgres:17`). + - `memory=<2GiB` remains a spurious condition until a failure on a larger VM generalizes it + away. This bias of single-example learning should be discussed in the paper. diff --git a/pkg/preflight/learn/learn_test.go b/pkg/preflight/learn/learn_test.go new file mode 100644 index 0000000..f05f85c --- /dev/null +++ b/pkg/preflight/learn/learn_test.go @@ -0,0 +1,115 @@ +package learn + +import ( + "regexp" + "testing" +) + +func TestExtractPicksSpecificCauseFromComposeOutput(t *testing.T) { + out := "Attaching to db-1\n" + + "db-1 | SQL Server 2022 will run as non-root by default.\n" + + "db-1 | /opt/mssql/bin/sqlservr: Invalid mapping of address 0x4005353000 in reserved address space below 0x400000000000. Possible causes:\n" + + "db-1 | 1) the process (itself, or via a wrapper) starts-up its own running environment\n" + + "db-1 exited with code 1\n" + f, ok := Extract(out) + if !ok || f.Service != "db" || f.ExitCode != 1 { + t.Fatalf("got %+v", f) + } + if want := "/opt/mssql/bin/sqlservr: Invalid mapping"; f.Line[:len(want)] != want { + t.Errorf("line = %q", f.Line) + } +} + +func TestExtractIgnoresGenericWrappers(t *testing.T) { + f, ok := Extract("Error response from daemon:\nERROR: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]\nsearch-1 exited with code 78\n") + if !ok || f.Service != "search" || f.ExitCode != 78 { + t.Fatalf("got %+v", f) + } +} + +func TestSignatureGeneralizesVolatileParts(t *testing.T) { + a := "/opt/mssql/bin/sqlservr: Invalid mapping of address 0x4005353000 in reserved address space below 0x400000000000. Possible causes:" + b := "/usr/bin/sqlservr: Invalid mapping of address 0x7f00aa in reserved address space below 0x400000000000. Possible causes:" + re := regexp.MustCompile(Signature(a)) + if !re.MatchString(a) || !re.MatchString(b) { + t.Errorf("signature %q should match both", Signature(a)) + } + if re.MatchString("exec format error") { + t.Error("signature too loose") + } +} + +func features(kv ...string) map[string]string { + m := map[string]string{} + for i := 0; i+1 < len(kv); i += 2 { + m[kv[i]] = kv[i+1] + } + return m +} + +func TestLearnGeneralizeAndRefine(t *testing.T) { + s := &Store{path: t.TempDir() + "/learned.yaml"} + crash := Failure{Line: "WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!", Service: "db"} + + // First failure: colima, QEMU, small VM. + out := s.ObserveFailure(crash, features("image", "docker.io/library/mongo", "run.platform", "linux/amd64", "host.platform", "linux/arm64", "emulation", "qemu", "memory", "<2GiB", "engine", "colima", "build", "yes", "buildkit", "no"), "mac-1") + r := out.Rule + if !out.Created || r.When["emulation"] != "qemu" || r.When["memory"] != "<2GiB" || r.When["image"] != "docker.io/library/mongo" { + t.Fatalf("new rule when = %v", r.When) + } + if _, ok := r.When["engine"]; ok { + t.Error("engine is recorded but not a starting condition") + } + if r.When["run.platform"] != "linux/amd64" || r.When["buildkit"] != "no" { + t.Errorf("foreign platform and builder of a built service are conditions: %v", r.When) + } + image := s.ObserveFailure(Failure{Line: "other failure xyz"}, features("image", "x", "run.platform", "linux/arm64", "host.platform", "linux/arm64", "build", "no", "buildkit", "no"), "m").Rule + if len(image.When) != 1 { + t.Errorf("native platform and unused builder must not be conditions: %v", image.When) + } + + // Same failure on a bigger VM: memory was not the cause. + out = s.ObserveFailure(crash, features("image", "docker.io/library/mongo", "run.platform", "linux/amd64", "host.platform", "linux/arm64", "emulation", "qemu", "memory", "<8GiB", "build", "yes", "buildkit", "yes"), "mac-2") + if out.Created || len(out.Dropped) != 2 || r.When["memory"] != "" || r.When["buildkit"] != "" { + t.Fatalf("generalization: dropped=%v when=%v", out.Dropped, r.When) + } + + // Mongo under Rosetta works: rule does not hold, untouched. + if n := len(s.ObserveSuccess("db", features("image", "docker.io/library/mongo", "run.platform", "linux/amd64", "emulation", "rosetta"), "mac-3")); n != 0 { + t.Errorf("rosetta success should not touch the rule") + } + if r.Status != Active { + t.Errorf("status = %s", r.Status) + } + + // A success that satisfies every condition but differs on a feature + // all failures share specializes the rule. + r.When = map[string]string{"image": "docker.io/library/mongo"} + touched := s.ObserveSuccess("db", features("image", "docker.io/library/mongo", "run.platform", "linux/arm64", "emulation", "none"), "linux-arm") + if len(touched) != 1 || r.When["run.platform"] != "linux/amd64" || r.Status != Active { + t.Errorf("specialize: when=%v status=%s", r.When, r.Status) + } + + if err := s.Save(); err != nil { + t.Fatal(err) + } +} + +func TestSuccessLearnsMissingVariable(t *testing.T) { + s := &Store{path: t.TempDir() + "/learned.yaml"} + fail := Failure{Line: "Error: Database is uninitialized and superuser password is not specified.", Service: "db"} + r := s.ObserveFailure(fail, features("image", "docker.io/library/postgres", "emulation", "none"), "mac").Rule + if len(r.When) != 1 { + t.Fatalf("when = %v", r.When) + } + s.ObserveSuccess("db", features("image", "docker.io/library/postgres", "emulation", "none", "env.POSTGRES_PASSWORD", "set"), "mac") + if r.When["env.POSTGRES_PASSWORD"] != unset || r.Status != Active { + t.Errorf("when = %v status = %s", r.When, r.Status) + } + if r.holds(features("image", "docker.io/library/postgres", "env.POSTGRES_PASSWORD", "set")) { + t.Error("rule must not fire once the password is set") + } + if !r.holds(features("image", "docker.io/library/postgres")) { + t.Error("rule must fire when the password is missing") + } +} diff --git a/pkg/preflight/learn/signature.go b/pkg/preflight/learn/signature.go new file mode 100644 index 0000000..93f673c --- /dev/null +++ b/pkg/preflight/learn/signature.go @@ -0,0 +1,150 @@ +// Package learn turns observed container failures into preflight rules. A +// failure's decisive log line becomes a signature; the project and host +// features at the time become the rule's conditions. Later failures with the +// same signature generalize the conditions, and successful runs that match +// them mark the rule unreliable. +package learn + +import ( + "regexp" + "strconv" + "strings" +) + +// Failure is what was extracted from a failed run's output. +type Failure struct { + Line string + Service string + ExitCode int +} + +// composePrefix matches Compose's log prefix, e.g. "db-1 | ". +var composePrefix = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]*?)(?:-\d+)?\s+\|\s?(.*)$`) + +// exitedLine matches Compose's container exit report. +var exitedLine = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]*?)(?:-\d+)? exited with code (\d+)`) + +var ansi = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +// Keywords that make a line decisive, with weights. Generic wrappers such as +// "exited with code" or "Error response from daemon" score low so the +// specific cause wins. +var cues = []struct { + re *regexp.Regexp + weight int +}{ + {regexp.MustCompile(`(?i)\b(fatal|panic|segmentation fault|illegal instruction|core dumped)\b`), 5}, + {regexp.MustCompile(`(?i)\b(requires?|required|not supported|unsupported|incompatible|too low|at least)\b`), 4}, + {regexp.MustCompile(`(?i)\b(error|failed|failure|invalid|cannot|can't|unable|denied|refused|not found|no such|no space|oom|killed)\b`), 3}, + {regexp.MustCompile(`(?i)\bwarn(ing)?\b`), 1}, +} + +var generic = regexp.MustCompile(`(?i)(exited with code|error response from daemon:?\s*$|^error:?\s*$|attaching to|gracefully stopping|aborting on container exit)`) + +// Extract picks the line that best explains why a run failed. +func Extract(output string) (Failure, bool) { + var best Failure + bestScore := 0 + exitCodes := map[string]int{} + for _, raw := range strings.Split(ansi.ReplaceAllString(output, ""), "\n") { + line := strings.TrimSpace(raw) + if line == "" { + continue + } + if m := exitedLine.FindStringSubmatch(line); m != nil { + if code, _ := strconv.Atoi(m[2]); code != 0 { + exitCodes[m[1]] = code + } + continue + } + service := "" + if m := composePrefix.FindStringSubmatch(line); m != nil { + service, line = m[1], strings.TrimSpace(m[2]) + } + score := 0 + for _, c := range cues { + if c.re.MatchString(line) { + score += c.weight + } + } + if generic.MatchString(line) { + score -= 3 + } + // Later lines win ties: the cause is usually printed last. + if score > 0 && score >= bestScore { + best, bestScore = Failure{Line: line, Service: service}, score + } + } + if bestScore == 0 { + return Failure{}, false + } + if best.Service == "" && len(exitCodes) == 1 { + for s := range exitCodes { + best.Service = s + } + } + best.ExitCode = exitCodes[best.Service] + if len(best.Line) > 300 { + best.Line = best.Line[:300] + } + return best, true +} + +var ( + hexAddr = regexp.MustCompile(`0x[0-9a-fA-F]+`) + hexID = regexp.MustCompile(`\b[0-9a-f]{12,64}\b`) + absPath = regexp.MustCompile(`(?:/[^\s"':,]+)+`) + number = regexp.MustCompile(`\d+`) + stampish = regexp.MustCompile(`^\S*\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\S*\s*`) +) + +// Signature turns a log line into a regular expression that matches the same +// failure on another machine: addresses, IDs, paths and numbers vary. +func Signature(line string) string { + line = stampish.ReplaceAllString(line, "") + type span struct { + start, end int + repl string + } + var spans []span + taken := make([]bool, len(line)) + mark := func(re *regexp.Regexp, repl string) { + for _, loc := range re.FindAllStringIndex(line, -1) { + free := true + for i := loc[0]; i < loc[1]; i++ { + if taken[i] { + free = false + break + } + } + if !free { + continue + } + for i := loc[0]; i < loc[1]; i++ { + taken[i] = true + } + spans = append(spans, span{loc[0], loc[1], repl}) + } + } + mark(hexAddr, `0x[0-9a-fA-F]+`) + mark(hexID, `[0-9a-f]+`) + mark(absPath, `\S+`) + mark(number, `\d+`) + + var b strings.Builder + for i := 0; i < len(line); { + matched := false + for _, sp := range spans { + if sp.start == i { + b.WriteString(sp.repl) + i, matched = sp.end, true + break + } + } + if !matched { + b.WriteString(regexp.QuoteMeta(line[i : i+1])) + i++ + } + } + return b.String() +} diff --git a/pkg/preflight/learn/store.go b/pkg/preflight/learn/store.go new file mode 100644 index 0000000..0851977 --- /dev/null +++ b/pkg/preflight/learn/store.go @@ -0,0 +1,338 @@ +package learn + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "gopkg.in/yaml.v3" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/rules" +) + +// File is where a project's learned rules live. It is meant to be committed, +// so a failure one teammate hits is predicted for everyone. +const File = ".container-doctor/learned.yaml" + +// relevant lists the features a new rule starts with. Host details that +// rarely cause failures (engine, Compose version) are recorded but not used +// as conditions until evidence requires them. +var relevant = []string{"image", "run.platform", "emulation", "memory", "buildkit"} + +// applies drops starting conditions that cannot be the cause: the builder +// for a service that is not built, or the platform when it is native. +func applies(k, v string, features map[string]string) bool { + switch k { + case "buildkit": + return features["build"] == "yes" + case "run.platform": + return v != features["host.platform"] + } + return true +} + +// normal values are the unremarkable state of a feature; a single failure +// does not make them part of a rule. +var normal = map[string]string{"emulation": "none", "buildkit": "yes", "memory": ">=8GiB"} + +type Store struct { + path string + Rules []*Learned `yaml:"rules"` +} + +type Learned struct { + ID string `yaml:"id"` + Signature string `yaml:"signature"` + Example string `yaml:"example"` + When map[string]string `yaml:"when"` + Fix string `yaml:"fix,omitempty"` + Status string `yaml:"status"` + Failures []Observation `yaml:"failures"` + Successes []Observation `yaml:"successes,omitempty"` +} + +type Observation struct { + At time.Time `yaml:"at"` + Host string `yaml:"host"` + Service string `yaml:"service,omitempty"` + ExitCode int `yaml:"exitCode,omitempty"` + Features map[string]string `yaml:"features"` +} + +const ( + Active = "active" + Unreliable = "unreliable" +) + +func Load(dir string) (*Store, error) { + s := &Store{path: filepath.Join(dir, File)} + b, err := os.ReadFile(s.path) + if errors.Is(err, os.ErrNotExist) { + return s, nil + } + if err != nil { + return nil, err + } + if err := yaml.Unmarshal(b, s); err != nil { + return nil, fmt.Errorf("%s: %w", s.path, err) + } + return s, nil +} + +func (s *Store) Save() error { + if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { + return err + } + b, err := yaml.Marshal(s) + if err != nil { + return err + } + header := "# Rules learned from real failures by `container-doctor learn`.\n# Commit this file so preflight predicts these failures for everyone.\n" + return os.WriteFile(s.path, append([]byte(header), b...), 0o644) +} + +func (s *Store) Path() string { return s.path } + +// Outcome says what an observation did to the store. +type Outcome struct { + Rule *Learned + Created bool + // Dropped lists conditions removed because a new failure did not share + // them (the rule became more general). + Dropped []string +} + +// ObserveFailure records a failure. A failure whose signature matches an +// existing rule generalizes it: conditions the two failures disagree on are +// dropped. Otherwise a new rule is created. +func (s *Store) ObserveFailure(f Failure, features map[string]string, hostName string) Outcome { + obs := Observation{At: time.Now().UTC(), Host: hostName, Service: f.Service, ExitCode: f.ExitCode, Features: features} + + for _, r := range s.Rules { + if !r.matchesLine(f.Line) { + continue + } + r.Failures = append(r.Failures, obs) + var dropped []string + for k, v := range r.When { + if value(features, k) != v { + delete(r.When, k) + dropped = append(dropped, k+"="+v) + } + } + sort.Strings(dropped) + r.Status = r.recomputeStatus() + return Outcome{Rule: r, Dropped: dropped} + } + + when := map[string]string{} + for _, k := range relevant { + v, ok := features[k] + if !ok || v == "" || v == "unknown" || normal[k] == v || !applies(k, v, features) { + continue + } + when[k] = v + } + r := &Learned{ + ID: s.nextID(features["image"]), + Signature: Signature(f.Line), + Example: f.Line, + When: when, + Status: Active, + Failures: []Observation{obs}, + } + s.Rules = append(s.Rules, r) + return Outcome{Rule: r, Created: true} +} + +// ObserveSuccess records a run that worked. Any active rule whose conditions +// all hold for it is contradicted: its conditions are too general. If the +// failures agree on a feature where the success differs, that feature is +// added as a condition (the rule is specialized); otherwise the rule is +// marked unreliable. +func (s *Store) ObserveSuccess(service string, features map[string]string, hostName string) []*Learned { + var touched []*Learned + for _, r := range s.Rules { + if !r.holds(features) { + continue + } + r.Successes = append(r.Successes, Observation{At: time.Now().UTC(), Host: hostName, Service: service, Features: features}) + if k, v, ok := r.distinguishing(features); ok { + r.When[k] = v + } + r.Status = r.recomputeStatus() + touched = append(touched, r) + } + return touched +} + +// distinguishing finds a feature on which every recorded failure agrees and +// the success differs. +func (r *Learned) distinguishing(success map[string]string) (string, string, bool) { + // Candidates include features only the success has: a variable the + // failures lacked is a condition "unset". + keys := map[string]bool{} + for k := range r.Failures[0].Features { + keys[k] = true + } + for k := range success { + keys[k] = true + } + var sorted []string + for k := range keys { + sorted = append(sorted, k) + } + // Prefer the curated features, then the rest alphabetically. + sort.SliceStable(sorted, func(i, j int) bool { + return rank(sorted[i]) < rank(sorted[j]) || (rank(sorted[i]) == rank(sorted[j]) && sorted[i] < sorted[j]) + }) + for _, k := range sorted { + if _, already := r.When[k]; already { + continue + } + v := value(r.Failures[0].Features, k) + agree := true + for _, f := range r.Failures[1:] { + if value(f.Features, k) != v { + agree = false + } + } + if agree && value(success, k) != v { + return k, v, true + } + } + return "", "", false +} + +func rank(k string) int { + for i, r := range relevant { + if r == k { + return i + } + } + return len(relevant) +} + +// recomputeStatus is unreliable while some success still satisfies every +// condition. +func (r *Learned) recomputeStatus() string { + for _, s := range r.Successes { + if r.holds(s.Features) { + return Unreliable + } + } + return Active +} + +func (r *Learned) holds(features map[string]string) bool { + for k, v := range r.When { + if value(features, k) != v { + return false + } + } + return true +} + +// unset is the value of a feature that was not observed, e.g. an +// environment variable the service does not define. +const unset = "unset" + +func value(features map[string]string, k string) string { + if v, ok := features[k]; ok && v != "" { + return v + } + return unset +} + +func (r *Learned) matchesLine(line string) bool { + re, err := regexp.Compile(r.Signature) + return err == nil && re.MatchString(line) +} + +// Matches reports whether line is this rule's failure. +func (r *Learned) Matches(line string) bool { return r.matchesLine(line) } + +func (s *Store) nextID(image string) string { + base := "learned" + if image != "" { + base += "." + strings.ReplaceAll(filepath.Base(image), "_", "-") + } + id := base + for n := 2; s.byID(id) != nil; n++ { + id = fmt.Sprintf("%s-%d", base, n) + } + return id +} + +func (s *Store) byID(id string) *Learned { + for _, r := range s.Rules { + if r.ID == id { + return r + } + } + return nil +} + +// Rules exposes the active learned rules to the preflight engine. +func (s *Store) Active() []rules.Rule { + var out []rules.Rule + for _, r := range s.Rules { + if r.Status == Active { + out = append(out, learnedRule{r}) + } + } + return out +} + +type learnedRule struct{ l *Learned } + +func (r learnedRule) ID() string { return r.l.ID } +func (r learnedRule) Title() string { return "Learned: " + r.l.Example } +func (r learnedRule) Needs() []fact.ID { + return []fact.ID{fact.Daemon, fact.ComposeFile} +} + +func (r learnedRule) Check(_ context.Context, env *rules.Env) []rules.Finding { + var out []rules.Finding + for _, svc := range env.Project.Services { + features := rules.Features(env, svc) + if len(r.l.When) == 0 || !r.l.holds(features) { + continue + } + var conds []string + for k, v := range r.l.When { + conds = append(conds, k+"="+v) + } + sort.Strings(conds) + first := r.l.Failures[0] + evidence := []string{ + fmt.Sprintf("learned: failed %d time(s), first on %s (%s)", len(r.l.Failures), first.Host, first.At.Format("2006-01-02")), + "conditions now true here: " + strings.Join(conds, ", "), + } + if n := len(r.l.Successes); n > 0 { + evidence = append(evidence, fmt.Sprintf("refined by %d successful run(s)", n)) + } + fix := r.l.Fix + if fix == "" { + fix = "Seen before under the same conditions. Change one of them, or add a fix to " + File + "." + } + out = append(out, rules.Finding{ + Rule: r.l.ID, + Severity: fact.Error, + Service: svc.Name, + Title: "this failed before under the same conditions: " + r.l.Example, + Evidence: evidence, + Fix: fix, + Location: svc.Location.String(), + Predicts: r.l.Example, + }) + } + return out +} diff --git a/pkg/preflight/preflight.go b/pkg/preflight/preflight.go index 99022aa..ccf1784 100644 --- a/pkg/preflight/preflight.go +++ b/pkg/preflight/preflight.go @@ -14,6 +14,7 @@ import ( "github.com/raphgm/container-doctor/internal/executor" "github.com/raphgm/container-doctor/pkg/preflight/fact" "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/learn" "github.com/raphgm/container-doctor/pkg/preflight/project" "github.com/raphgm/container-doctor/pkg/preflight/registry" "github.com/raphgm/container-doctor/pkg/preflight/rules" @@ -67,18 +68,15 @@ func (r *Report) Errors() int { return n } -func Run(ctx context.Context, opts Options) (*Report, error) { - start := time.Now() +// Prepare loads the project, profiles the host and resolves images: every +// input the rules read. +func Prepare(ctx context.Context, opts Options) (*rules.Env, error) { if opts.Runner == nil { opts.Runner = executor.New() } if opts.Resolver == nil { opts.Resolver = registry.NewRemote() } - if opts.Rules == nil { - opts.Rules = rules.All() - } - var h *host.Profile facts := fact.NewSet() if opts.Host != nil { @@ -102,13 +100,36 @@ func Run(ctx context.Context, opts Options) (*Report, error) { } else { env.Images = resolveImages(ctx, opts.Resolver, rules.Pulls(proj, h), facts) } + return env, nil +} +// Run predicts the failures of the project in opts.Dir. +func Run(ctx context.Context, opts Options) (*Report, error) { + start := time.Now() + env, err := Prepare(ctx, opts) + if err != nil { + return nil, err + } + if opts.Rules == nil { + opts.Rules = rules.All() + learned, err := learn.Load(env.Project.Dir) + if err != nil { + return nil, err + } + opts.Rules = append(opts.Rules, learned.Active()...) + } + return Evaluate(ctx, env, opts.Rules, opts.Host, start), nil +} + +// Evaluate runs rules against a prepared environment. +func Evaluate(ctx context.Context, env *rules.Env, rs []rules.Rule, snap *host.Snapshot, start time.Time) *Report { + proj, h, facts := env.Project, env.Host, env.Facts rep := &Report{Project: proj.Name, Dir: proj.Dir, Host: h, Timestamp: start} - if opts.Host != nil { - rep.HostName = opts.Host.Hostname + if snap != nil { + rep.HostName = snap.Hostname } blocked := map[fact.ID][]string{} - for _, r := range opts.Rules { + for _, r := range rs { if root, ok := blockingRoot(facts, r.Needs()); ok { blocked[root.Fact] = append(blocked[root.Fact], r.ID()) continue @@ -127,7 +148,7 @@ func Run(ctx context.Context, opts Options) (*Report, error) { return rank(rep.Findings[i].Severity) < rank(rep.Findings[j].Severity) }) rep.Duration = time.Since(start) - return rep, nil + return rep } func blockingRoot(facts *fact.Set, needs []fact.ID) (fact.Failure, bool) { diff --git a/pkg/preflight/rules/features.go b/pkg/preflight/rules/features.go new file mode 100644 index 0000000..03a8492 --- /dev/null +++ b/pkg/preflight/rules/features.go @@ -0,0 +1,154 @@ +package rules + +import ( + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + + "github.com/raphgm/container-doctor/pkg/preflight/host" + "github.com/raphgm/container-doctor/pkg/preflight/project" + "github.com/raphgm/container-doctor/pkg/preflight/registry" +) + +// Features describes one service on one host as flat key/value pairs. +// Learned rules are conjunctions over these keys, so the vocabulary is kept +// small and host-independent where possible (buckets, not exact numbers). +func Features(env *Env, svc *project.Service) map[string]string { + h := env.Host + f := map[string]string{ + "host.platform": hostPlatform(h), + "engine": engineKind(h), + "memory": memoryBucket(h.MemTotal), + "buildkit": yesNo(h.BuildxInstalled && !h.BuildKitDisabled), + } + if v := h.ComposeVersion; v != "" { + f["compose.major"] = strings.SplitN(v, ".", 2)[0] + } + + f["build"] = yesNo(svc.Build != nil) + + // Environment variables are recorded as set/empty, never their values: + // learned rules are shared and must not leak secrets. A variable that is + // absent reads as "unset" (see learn). + for k, v := range svc.Environment { + if v == "" { + f["env."+k] = "empty" + } else { + f["env."+k] = "set" + } + } + + ref := serviceImage(svc) + if ref == "" { + return f + } + f["image"] = repository(ref) + + want := hostPlatform(h) + if svc.Platform != "" { + want = svc.Platform + } + runAs := registry.Normalize(want) + if res, ok := env.Images[ImageKey(ref, want)]; ok && res.Image != nil { + ps := append([]string(nil), res.Image.Platforms...) + sort.Strings(ps) + f["image.platforms"] = strings.Join(ps, ",") + available := false + for _, p := range ps { + if registry.Compatible(p, want) { + available = true + } + } + if !available && len(ps) == 1 { + runAs = ps[0] + } + } + f["run.platform"] = runAs + + switch native, emulated := h.CanRun(runAs); { + case native: + f["emulation"] = "none" + case emulated && h.Emulators[runAs] != "": + f["emulation"] = h.Emulators[runAs] + case emulated: + f["emulation"] = "yes" + case h.EmulationKnown: + f["emulation"] = "unavailable" + default: + f["emulation"] = "unknown" + } + return f +} + +// serviceImage is the image the service's containers run: its image, or the +// base of the final build stage. +func serviceImage(svc *project.Service) string { + if svc.Image != "" { + return svc.Image + } + if svc.Build == nil || svc.Build.Dockerfile == nil || len(svc.Build.Dockerfile.Stages) == 0 { + return "" + } + df := svc.Build.Dockerfile + reach, err := df.Reachable(svc.Build.Target) + if err != nil || len(reach) == 0 { + return "" + } + st := df.Stages[reach[len(reach)-1]] + for st.BaseStage >= 0 { + st = df.Stages[st.BaseStage] + } + if st.Unresolved() || strings.EqualFold(st.Base, "scratch") { + return "" + } + return st.Base +} + +// repository strips the tag or digest, so a rule learned on mongo:7 also +// covers mongo:7.0.14. +func repository(ref string) string { + parsed, err := name.ParseReference(ref) + if err != nil { + return ref + } + return parsed.Context().Name() +} + +func engineKind(h *host.Profile) string { + switch { + case h.OperatingSystem == "": + return "unknown" + case h.IsDesktop(): + return "docker-desktop" + case strings.Contains(h.Endpoint, "/.colima/"): + return "colima" + case strings.Contains(h.Endpoint, "/.orbstack/") || strings.Contains(h.OperatingSystem, "OrbStack"): + return "orbstack" + case !h.Local: + return "remote" + } + return "linux" +} + +func memoryBucket(n int64) string { + const gib = 1 << 30 + switch { + case n == 0: + return "unknown" + case n < 2*gib: + return "<2GiB" + case n < 4*gib: + return "<4GiB" + case n < 8*gib: + return "<8GiB" + } + return ">=8GiB" +} + +func yesNo(b bool) string { + if b { + return "yes" + } + return "no" +} From 74962e9a22e65a869f826b45a2ddd147709fc39e Mon Sep 17 00:00:00 2001 From: raphgm Date: Wed, 23 Sep 2026 23:04:22 +0100 Subject: [PATCH 04/23] bench: mine and verify GitHub issues per failure category mine.py counts GitHub issues per Docker failure category (including categories preflight does not cover) and samples 30 each; precision.py checks each sample for the exact error text to estimate precision and an adjusted count. --- bench/data/address-already-in-use.json | 280 ++++++++++++++++++ .../app-config-env-missing-at-runtime.json | 280 ++++++++++++++++++ bench/data/bind--dir-onto-file.json | 280 ++++++++++++++++++ bench/data/bind--source-does-not-exist.json | 280 ++++++++++++++++++ bench/data/build--buildkit-required.json | 280 ++++++++++++++++++ bench/data/build--copy-failed-legacy.json | 280 ++++++++++++++++++ bench/data/build--copy-source-not-found.json | 280 ++++++++++++++++++ bench/data/build--outside-build-context.json | 280 ++++++++++++++++++ bench/data/compose--required-variable.json | 280 ++++++++++++++++++ bench/data/compose--unsupported-property.json | 280 ++++++++++++++++++ bench/data/compose--variable-not-set.json | 280 ++++++++++++++++++ .../data/container-healthcheck-unhealthy.json | 280 ++++++++++++++++++ bench/data/counts.csv | 33 +++ bench/data/credentials-helper-missing.json | 280 ++++++++++++++++++ bench/data/daemon--cannot-connect.json | 280 ++++++++++++++++++ bench/data/desktop--mounts-denied.json | 280 ++++++++++++++++++ bench/data/disk--no-space-left.json | 280 ++++++++++++++++++ bench/data/dns-resolution-in-container.json | 280 ++++++++++++++++++ bench/data/emulation--qemu-crash.json | 280 ++++++++++++++++++ bench/data/gpu--no-device-driver.json | 280 ++++++++++++++++++ bench/data/image--pull-access-denied.json | 280 ++++++++++++++++++ bench/data/image--tag-repo-not-found.json | 280 ++++++++++++++++++ bench/data/kernel--vm-max-map-count.json | 280 ++++++++++++++++++ bench/data/memory--mssql-minimum.json | 280 ++++++++++++++++++ bench/data/memory--oomkilled.json | 280 ++++++++++++++++++ bench/data/network-not-found.json | 280 ++++++++++++++++++ bench/data/oci-runtime-exec-failed.json | 280 ++++++++++++++++++ bench/data/platform--exec-format-error.json | 280 ++++++++++++++++++ .../data/platform--no-matching-manifest.json | 271 +++++++++++++++++ ...platform--requested-platform-mismatch.json | 280 ++++++++++++++++++ bench/data/port-already-allocated.json | 280 ++++++++++++++++++ bench/data/rootless-privileged-port.json | 280 ++++++++++++++++++ bench/data/volume-permission-denied.json | 280 ++++++++++++++++++ bench/mine.py | 94 ++++++ bench/precision.py | 84 ++++++ 35 files changed, 9162 insertions(+) create mode 100644 bench/data/address-already-in-use.json create mode 100644 bench/data/app-config-env-missing-at-runtime.json create mode 100644 bench/data/bind--dir-onto-file.json create mode 100644 bench/data/bind--source-does-not-exist.json create mode 100644 bench/data/build--buildkit-required.json create mode 100644 bench/data/build--copy-failed-legacy.json create mode 100644 bench/data/build--copy-source-not-found.json create mode 100644 bench/data/build--outside-build-context.json create mode 100644 bench/data/compose--required-variable.json create mode 100644 bench/data/compose--unsupported-property.json create mode 100644 bench/data/compose--variable-not-set.json create mode 100644 bench/data/container-healthcheck-unhealthy.json create mode 100644 bench/data/counts.csv create mode 100644 bench/data/credentials-helper-missing.json create mode 100644 bench/data/daemon--cannot-connect.json create mode 100644 bench/data/desktop--mounts-denied.json create mode 100644 bench/data/disk--no-space-left.json create mode 100644 bench/data/dns-resolution-in-container.json create mode 100644 bench/data/emulation--qemu-crash.json create mode 100644 bench/data/gpu--no-device-driver.json create mode 100644 bench/data/image--pull-access-denied.json create mode 100644 bench/data/image--tag-repo-not-found.json create mode 100644 bench/data/kernel--vm-max-map-count.json create mode 100644 bench/data/memory--mssql-minimum.json create mode 100644 bench/data/memory--oomkilled.json create mode 100644 bench/data/network-not-found.json create mode 100644 bench/data/oci-runtime-exec-failed.json create mode 100644 bench/data/platform--exec-format-error.json create mode 100644 bench/data/platform--no-matching-manifest.json create mode 100644 bench/data/platform--requested-platform-mismatch.json create mode 100644 bench/data/port-already-allocated.json create mode 100644 bench/data/rootless-privileged-port.json create mode 100644 bench/data/volume-permission-denied.json create mode 100644 bench/mine.py create mode 100644 bench/precision.py diff --git a/bench/data/address-already-in-use.json b/bench/data/address-already-in-use.json new file mode 100644 index 0000000..272ab49 --- /dev/null +++ b/bench/data/address-already-in-use.json @@ -0,0 +1,280 @@ +{ + "category": "address already in use", + "rule": "ports.host", + "query": "\"address already in use\" docker is:issue created:>=2021-01-01", + "total": 3792, + "sample": [ + { + "url": "https://github.com/NginxProxyManager/nginx-proxy-manager/issues/5852", + "title": "Address already in use", + "repo": "NginxProxyManager/nginx-proxy-manager", + "created": "2026-09-11T17:46:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/overcast-sh/overcast/issues/2005", + "title": "test flake: TestRunTask_withDocker_debuggerInspectorAnswersThroughTheProxy fails with 'address already in use' when Docker publishes the debug port", + "repo": "overcast-sh/overcast", + "created": "2026-09-20T04:29:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tiliondev/fortress/issues/38", + "title": "Container self-exits with \"socat ... bind :9222: Address already in use\" after 1-2 requests", + "repo": "tiliondev/fortress", + "created": "2026-09-17T15:14:06Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/microsoft/aspire/issues/19014", + "title": "[CI Failure] Flaky: AzureSqlPrincipalReconciliationTests fail because SqlServerContainerFixture cannot bind port 1433 (address already in use)", + "repo": "microsoft/aspire", + "created": "2026-08-05T16:15:39Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kavix/why/issues/8", + "title": "[Adapter] Docker Diagnostic Adapter: why docker compose up", + "repo": "kavix/why", + "created": "2026-09-20T03:39:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/microsoft/aspire/issues/12247", + "title": "`dotnet run` fails with \"address already in use\" for fixed external ports via `.WithEndpoint`", + "repo": "microsoft/aspire", + "created": "2025-10-21T12:59:06Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/tobychui/zoraxy/issues/1117", + "title": "[BUG] Cannot generate certificate from ZeroSSL. listen tcp :56563: bind: address already in use", + "repo": "tobychui/zoraxy", + "created": "2026-04-14T08:28:51Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/moby/moby/issues/47944", + "title": "False address already in use", + "repo": "moby/moby", + "created": "2024-06-10T18:21:41Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/moby/moby/issues/53153", + "title": "28->29 Regression: RootlessKit PortManager.AddPort(): listen tcp4 0.0.0.0:32769: bind: address already in use", + "repo": "moby/moby", + "created": "2026-07-23T15:14:09Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/ostis-apps/nika/issues/192", + "title": "Error. Address already in use.", + "repo": "ostis-apps/nika", + "created": "2023-01-04T13:39:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/aiordache/demos/issues/2", + "title": "address already in use", + "repo": "aiordache/demos", + "created": "2021-06-17T09:20:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/stilliard/docker-pure-ftpd/issues/159", + "title": "bind: Address already in use", + "repo": "stilliard/docker-pure-ftpd", + "created": "2021-04-26T19:03:01Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/EtherealEngine/Digital-Beings/issues/592", + "title": "address already in use", + "repo": "EtherealEngine/Digital-Beings", + "created": "2023-10-14T12:24:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/aschmelyun/subvert/issues/38", + "title": "Docker Install Error: bind: address already in use.", + "repo": "aschmelyun/subvert", + "created": "2024-06-25T19:14:26Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/JunYupK/CDC-PipeLine-With-Kafka/issues/12", + "title": "[Docker] docker oserror: [errno 98] address already in use", + "repo": "JunYupK/CDC-PipeLine-With-Kafka", + "created": "2025-01-23T00:59:24Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/getumbrel/umbrel/issues/2213", + "title": "tailscale serve claims ports with no coordination against app_proxy's Docker port-publish \u2014 causes app containers to fail with \"address already in use\"", + "repo": "getumbrel/umbrel", + "created": "2026-08-28T18:28:01Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/localagi/gpt-code-ui-docker/issues/1", + "title": "Address already in use, Connection refused ", + "repo": "localagi/gpt-code-ui-docker", + "created": "2023-07-12T08:32:42Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/jasonacox/Powerwall-Dashboard/issues/605", + "title": " failed to bind port - address already in use", + "repo": "jasonacox/Powerwall-Dashboard", + "created": "2025-04-02T23:00:31Z", + "state": "open", + "comments": 19, + "verified": true + }, + { + "url": "https://github.com/aden-hive/hive/issues/6968", + "title": "[Bug]:error while attempting to bind on address ('127.0.0.1', 8787): address already in use", + "repo": "aden-hive/hive", + "created": "2026-04-07T05:19:20Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/microsoft/WSL/issues/40984", + "title": "[WSL 2.9.3] Docker fails to bind ports with \"address already in use\" when networkingMode=mirrored is enabled", + "repo": "microsoft/WSL", + "created": "2026-07-02T17:10:09Z", + "state": "open", + "comments": 28, + "verified": true + }, + { + "url": "https://github.com/hashicorp/vagrant-vmware-desktop/issues/96", + "title": "VMWare Utility address already in use", + "repo": "hashicorp/vagrant-vmware-desktop", + "created": "2021-11-11T07:59:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/dmohanja/GT7Monitor/issues/5", + "title": "Error Address already in use", + "repo": "dmohanja/GT7Monitor", + "created": "2025-03-04T03:24:57Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Banh-Canh/docker-FMD2/issues/13", + "title": "[errno 98] Address already in use", + "repo": "Banh-Canh/docker-FMD2", + "created": "2023-11-04T20:50:35Z", + "state": "open", + "comments": 42, + "verified": true + }, + { + "url": "https://github.com/canonical/edgex-snap-testing/issues/118", + "title": "Bind address already in use", + "repo": "canonical/edgex-snap-testing", + "created": "2022-08-15T06:58:14Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/bigcommerce/stencil-cli/issues/1366", + "title": "Docker: will not start, Error: listen EADDRINUSE: address already in use :::3001", + "repo": "bigcommerce/stencil-cli", + "created": "2026-04-11T18:23:23Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/qdm12/port-checker/issues/13", + "title": "Fatal error: starting http server: listen tcp :: bind: address already in use", + "repo": "qdm12/port-checker", + "created": "2026-01-16T09:04:10Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/lgsvl/lanefollowing/issues/17", + "title": " what(): bind: Address already in use", + "repo": "lgsvl/lanefollowing", + "created": "2022-03-03T09:55:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/lgsvl/simulator/issues/2009", + "title": " what(): bind: Address already in use", + "repo": "lgsvl/simulator", + "created": "2022-03-03T09:55:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/kata-containers/kata-containers/issues/6427", + "title": "error=\"listen unix /run/kata/test/shim-monitor.sock: bind: address already in use", + "repo": "kata-containers/kata-containers", + "created": "2023-03-08T12:51:40Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/KurtzL/nestjs-temporal/issues/54", + "title": "Worker restarts: address already in use", + "repo": "KurtzL/nestjs-temporal", + "created": "2024-06-10T08:17:37Z", + "state": "open", + "comments": 2, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 3792 +} \ No newline at end of file diff --git a/bench/data/app-config-env-missing-at-runtime.json b/bench/data/app-config-env-missing-at-runtime.json new file mode 100644 index 0000000..8228c50 --- /dev/null +++ b/bench/data/app-config-env-missing-at-runtime.json @@ -0,0 +1,280 @@ +{ + "category": "app config/env missing at runtime", + "rule": "(learned)", + "query": "\"Database is uninitialized and superuser password is not specified\" is:issue created:>=2021-01-01", + "total": 118, + "sample": [ + { + "url": "https://github.com/attevon-llc/OpenTranscribe/issues/961", + "title": "fix(docker): a --fresh stack started from a git worktree fails twice \u2014 no .env, and the second Vite dev server hits EMFILE", + "repo": "attevon-llc/OpenTranscribe", + "created": "2026-09-20T20:01:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/open-horizon-services/service-postgresql/issues/5", + "title": "ERROR: Error message being thrown in docker logs", + "repo": "open-horizon-services/service-postgresql", + "created": "2025-04-08T15:18:34Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Crivaledaz/Mattermost-LDAP/issues/82", + "title": "Database not being created in container?", + "repo": "Crivaledaz/Mattermost-LDAP", + "created": "2021-11-02T16:00:52Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/qdrtech/self-hosted-ai/issues/3", + "title": "Postgress database is uninitilized", + "repo": "qdrtech/self-hosted-ai", + "created": "2024-12-22T22:48:22Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/mattermost/docker/issues/163", + "title": "Any support with podman-compose?", + "repo": "mattermost/docker", + "created": "2025-03-06T06:39:06Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/asc-lab/dotnetcore-microservices-poc/issues/58", + "title": "Unable to start Postgres from infra-run.sh file ", + "repo": "asc-lab/dotnetcore-microservices-poc", + "created": "2023-08-29T13:20:03Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/divio/divio-cli/issues/243", + "title": "Database configuration in `divio app setup` not working correctly", + "repo": "divio/divio-cli", + "created": "2021-10-28T14:01:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker-library/postgres/issues/1339", + "title": "Container in Ubuntu aarch64 machine cannot use Docker Secrets feature", + "repo": "docker-library/postgres", + "created": "2025-05-08T07:36:31Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/satoshipay/docker-stellar-core/issues/33", + "title": "Issues with initializing db", + "repo": "satoshipay/docker-stellar-core", + "created": "2021-06-26T22:39:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/delner/docker-training/issues/1", + "title": "Postgres needs POSTGRES_PASSWORD at startup", + "repo": "delner/docker-training", + "created": "2021-01-12T07:58:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/akornatskyy/scheduler/issues/815", + "title": "Docker Compose instructions are not instructions", + "repo": "akornatskyy/scheduler", + "created": "2022-12-13T07:45:21Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/hashicorp/waypoint/issues/2975", + "title": "environment variables in waypoint.hcl not getting passed to the docker deployed container ", + "repo": "hashicorp/waypoint", + "created": "2022-02-03T04:48:11Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/haswanth13901/Zenoscribe/issues/50", + "title": "Document the Dependabot secret store in docs/github-repo-settings.md \u00a72", + "repo": "haswanth13901/Zenoscribe", + "created": "2026-08-27T07:13:13Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/shamedgh/confine/issues/14", + "title": "Options in image input file (`images.json`) are not parsed correctly", + "repo": "shamedgh/confine", + "created": "2023-05-05T19:43:33Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/wtfloris/hestia/issues/80", + "title": "Generate reaction message that can be easily copied", + "repo": "wtfloris/hestia", + "created": "2025-05-06T17:53:11Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/dotnet/tye/issues/1255", + "title": "Tye on Podman with Postgres couldn't inject environment variables", + "repo": "dotnet/tye", + "created": "2021-12-30T08:04:02Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/taimg1/shipkit/issues/19", + "title": "Only KAMAL_REGISTRY_PASSWORD reaches Kamal: every other declared secret silently resolves to empty", + "repo": "taimg1/shipkit", + "created": "2026-09-16T17:58:15Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dyne/reflow-os/issues/25", + "title": "Initialization fails on MACOS with readme instructions", + "repo": "dyne/reflow-os", + "created": "2022-01-14T11:54:21Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/metanull/appsec-scout-2/issues/508", + "title": "Park the postgres service behind the local-db profile in docker-compose.ghcr.yml", + "repo": "metanull/appsec-scout-2", + "created": "2026-09-14T15:50:14Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/testcontainers/testcontainers-native/issues/69", + "title": "Failed container starts aren't always reported", + "repo": "testcontainers/testcontainers-native", + "created": "2026-02-02T21:02:34Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/6760", + "title": "Unable to bind to address other than 127.0.0.1 or 0.0.0.0", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2024-04-23T22:09:35Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/oikawa-d/task_management/issues/481", + "title": "[task] CD\u5ec3\u6b62\u65b9\u91dd\u3078\u306e\u5207\u308a\u66ff\u3048\u3068\u5c06\u6765\u306erunner\u624b\u9806\u6574\u7406", + "repo": "oikawa-d/task_management", + "created": "2026-09-15T06:25:15Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/trydirect/stacker/issues/240", + "title": "stacker deploy --target server writes every project to the same remote path, causing real cross-project deployment damage", + "repo": "trydirect/stacker", + "created": "2026-08-21T11:34:59Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/EnterpriseDB/docs/issues/6920", + "title": "Regarding \"Installing PostgreSQL in a Docker container on your local machine\"", + "repo": "EnterpriseDB/docs", + "created": "2025-07-25T04:49:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/clertonraf/satzwerk/issues/315", + "title": "Production 'satzwerk' Portainer stack has no DB_PASSWORD/JWT_SECRET configured - backend+postgres have never actually run on 192.168.0.110", + "repo": "clertonraf/satzwerk", + "created": "2026-09-15T10:09:25Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/coollabsio/coolify/issues/9029", + "title": "[Bug]: PR Preview: POSTGRES_PASSWORD interpolation fails because DB_PASSWORD is not injected into .env", + "repo": "coollabsio/coolify", + "created": "2026-03-18T13:37:19Z", + "state": "closed", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/projectcaluma/alexandria/issues/421", + "title": "Quick start not working", + "repo": "projectcaluma/alexandria", + "created": "2024-01-18T12:14:33Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/alchemy-run/alchemy/issues/1117", + "title": "Docker.Container passes environment names without values", + "repo": "alchemy-run/alchemy", + "created": "2026-08-06T06:56:24Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/rajnathsah/smartflow-engine/issues/8", + "title": "Error in starting application using docker compose", + "repo": "rajnathsah/smartflow-engine", + "created": "2026-06-03T03:17:23Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/s3rius/FastAPI-template/issues/174", + "title": "Project starts with Docker error.", + "repo": "s3rius/FastAPI-template", + "created": "2023-06-22T09:04:10Z", + "state": "open", + "comments": 23, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 118 +} \ No newline at end of file diff --git a/bench/data/bind--dir-onto-file.json b/bench/data/bind--dir-onto-file.json new file mode 100644 index 0000000..7a9f5ae --- /dev/null +++ b/bench/data/bind--dir-onto-file.json @@ -0,0 +1,280 @@ +{ + "category": "bind: dir onto file", + "rule": "mounts.bind", + "query": "\"Are you trying to mount a directory onto a file\" is:issue created:>=2021-01-01", + "total": 707, + "sample": [ + { + "url": "https://github.com/nothingmn/echonotes/issues/2", + "title": "summarize-notes.md: not a directory: unknown: Are you trying to mount a directory onto a file", + "repo": "nothingmn/echonotes", + "created": "2025-07-27T21:18:27Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/portainer/portainer/issues/11023", + "title": "Git Compose Deploy Issue - flags: 0x5000: not a directory: unknown: Are you trying to mount a directory onto a file", + "repo": "portainer/portainer", + "created": "2024-01-27T03:13:20Z", + "state": "open", + "comments": 13, + "verified": true + }, + { + "url": "https://github.com/open-webui/oikb/issues/116", + "title": "Docker and .oikb.yaml", + "repo": "open-webui/oikb", + "created": "2026-09-03T06:51:28Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/bassmaster187/TeslaLogger/issues/999", + "title": "Error after starting docker-compose - Are you trying to mount a directory onto a file (or vice-versa)?", + "repo": "bassmaster187/TeslaLogger", + "created": "2023-04-29T17:48:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/pneumaticapp/pneumaticworkflow/issues/336", + "title": "Docker fails to run: seaweedfs-filer fails to mount filer.toml", + "repo": "pneumaticapp/pneumaticworkflow", + "created": "2026-08-11T15:08:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/GispoCoding/geoviz-notebooks/issues/48", + "title": "Restarting server with docker-compose may crash with \"Are you trying to mount a directory onto a file (or vice-versa)?\"", + "repo": "GispoCoding/geoviz-notebooks", + "created": "2022-01-24T10:49:47Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/moesnow/March7thAssistant/issues/1187", + "title": "[Bug]:\u4f7f\u7528\u9884\u7f16\u8bd1\u955c\u50cf\u65f6\u5c06config\u6587\u4ef6\u521b\u5efa\u6210\u6587\u4ef6\u5939", + "repo": "moesnow/March7thAssistant", + "created": "2026-08-19T11:39:52Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/NeonGeckoCom/neon-hub-installer/issues/31", + "title": "[BUG] Issues in Cloud Deployment docs", + "repo": "NeonGeckoCom/neon-hub-installer", + "created": "2026-08-19T00:52:21Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/bililive-go/bililive-go/issues/811", + "title": "\u4f7f\u7528docker-compose\u90e8\u7f72\u65f6\uff0c\u7a0b\u5e8f\u628aconfig.docker.yml\u6587\u4ef6\u521b\u5efa\u6210\u4e86\u540d\u4e3a\u6587\u4ef6\u5939\uff0c\u5bfc\u81f4\u7a0b\u5e8f\u62a5\u9519", + "repo": "bililive-go/bililive-go", + "created": "2024-11-28T13:56:34Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/RyuKosei/PACEbench/issues/1", + "title": "[CORAZA-WAF] Benchmark cannot launch", + "repo": "RyuKosei/PACEbench", + "created": "2026-09-03T00:10:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/souvikg10/rasa_multi_bot/issues/3", + "title": "ERROR: for rasa_multi_bot-master_rasa_fr_1", + "repo": "souvikg10/rasa_multi_bot", + "created": "2021-01-26T05:11:11Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/qcgzxw/ebook-sender-bot/issues/63", + "title": "ERROR: Encountered errors while bringing up the project.", + "repo": "qcgzxw/ebook-sender-bot", + "created": "2023-01-28T09:07:03Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/robincher/docker-compose-ghost-quickstart/issues/1", + "title": "Starting ghost ... error", + "repo": "robincher/docker-compose-ghost-quickstart", + "created": "2021-07-03T12:36:26Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/linagora/linshare/issues/280", + "title": "Some docker container not running during installation ", + "repo": "linagora/linshare", + "created": "2023-08-18T07:08:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/shamhi/HamsterKombatBot/issues/1567", + "title": "ERROR: for HamsterKombatBot Cannot start service bot: failed", + "repo": "shamhi/HamsterKombatBot", + "created": "2024-07-14T11:53:37Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/angristan/feedbin-docker/issues/73", + "title": "Can't start Caddy container", + "repo": "angristan/feedbin-docker", + "created": "2021-06-09T02:14:19Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/setiseta/docker-librenms/issues/44", + "title": "Failed to mount volume of maridb", + "repo": "setiseta/docker-librenms", + "created": "2022-01-14T03:25:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/canyanio/janus-gateway-docker/issues/29", + "title": "Error running docker compose", + "repo": "canyanio/janus-gateway-docker", + "created": "2022-03-18T12:04:30Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Open-Location-Stack/open-location-hub/issues/59", + "title": "local-hub want to create clickhouse but config files appear as directories", + "repo": "Open-Location-Stack/open-location-hub", + "created": "2026-06-26T09:10:08Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/winboat-org/winboat/issues/354", + "title": "Command failed: docker container start WinBoat -> ventoy (\"Winboat Guest Api offline - Container exiting\")", + "repo": "winboat-org/winboat", + "created": "2025-10-16T14:13:49Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/mariomare22/docker-pihole-unbound/issues/17", + "title": "Cannot start service pihole: OCI runtime create failed: (Happening on Pi4)", + "repo": "mariomare22/docker-pihole-unbound", + "created": "2023-04-28T01:39:07Z", + "state": "open", + "comments": 15, + "verified": true + }, + { + "url": "https://github.com/denverdino/lxcfs-admission-webhook/issues/17", + "title": "pod \u51fa\u73b0CrashLoopBackOff", + "repo": "denverdino/lxcfs-admission-webhook", + "created": "2021-04-01T11:35:16Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/exment-git/docker-exment/issues/3", + "title": "Docker toolbox \u3067\u306f\u52d5\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "repo": "exment-git/docker-exment", + "created": "2021-12-23T16:08:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/paschmann/changd/issues/3", + "title": "Problem starting ui_server", + "repo": "paschmann/changd", + "created": "2022-01-19T12:35:29Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/4286", + "title": "Failure to start container with file mounts after windows reboot or wsl restart", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2023-03-27T11:34:01Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/BillyOutlast/rocm-automated/issues/2", + "title": "Error while deploying in portainer", + "repo": "BillyOutlast/rocm-automated", + "created": "2026-02-07T06:03:30Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dwaaan/HRConvert2-Docker/issues/11", + "title": "Error: The command failed, on Unraid server, please help!", + "repo": "dwaaan/HRConvert2-Docker", + "created": "2022-09-20T02:22:00Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/stonier/groot_rocker_extensions/issues/27", + "title": "Ssh temporary agents don't persist (conflicts with --persistant)", + "repo": "stonier/groot_rocker_extensions", + "created": "2021-04-02T16:31:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/anse-app/chatgpt-demo/issues/110", + "title": "docker: Error response from daemon: failed to create shim task", + "repo": "anse-app/chatgpt-demo", + "created": "2023-03-05T15:11:35Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/Cloud-RF/tak-server/issues/85", + "title": "Error during setup", + "repo": "Cloud-RF/tak-server", + "created": "2026-05-06T14:24:22Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 707 +} \ No newline at end of file diff --git a/bench/data/bind--source-does-not-exist.json b/bench/data/bind--source-does-not-exist.json new file mode 100644 index 0000000..8edccb7 --- /dev/null +++ b/bench/data/bind--source-does-not-exist.json @@ -0,0 +1,280 @@ +{ + "category": "bind: source does not exist", + "rule": "mounts.bind", + "query": "\"bind source path does not exist\" is:issue created:>=2021-01-01", + "total": 544, + "sample": [ + { + "url": "https://github.com/BenjaminBenetti/fleet-man/issues/241", + "title": "macOS: intermittent 'bind source path does not exist' for freshly recreated workspace dirs (Docker Desktop VirtioFS race)", + "repo": "BenjaminBenetti/fleet-man", + "created": "2026-07-21T18:36:48Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tribut/homeassistant-docker-venv/issues/35", + "title": "--mount: \"bind source path does not exist\", but it does", + "repo": "tribut/homeassistant-docker-venv", + "created": "2024-08-03T07:45:27Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/coder/coder/issues/19708", + "title": "bug: `coder_devcontainer` fails with \"bind source path does not exist\" for existing directories in Kubernetes deployment", + "repo": "coder/coder", + "created": "2025-09-04T22:08:23Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/LucienShui/PasteMe/issues/127", + "title": "invalid mount config for type \"bind\": bind source path does not exist", + "repo": "LucienShui/PasteMe", + "created": "2021-10-27T01:45:13Z", + "state": "open", + "comments": 17, + "verified": true + }, + { + "url": "https://github.com/common-workflow-language/cwltool/issues/1887", + "title": "bind source path does not exist - cwltool keeps waiting / running does not stop", + "repo": "common-workflow-language/cwltool", + "created": "2023-08-17T13:47:58Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/XedinUnknown/red-acre-test-a-1/issues/4", + "title": "invalid mount config for type \"bind\": bind source path does not exist", + "repo": "XedinUnknown/red-acre-test-a-1", + "created": "2023-07-13T17:13:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/derio-net/super-fr/issues/578", + "title": "fr isolation up: a failed postCreate leaves worktree + container with no state record", + "repo": "derio-net/super-fr", + "created": "2026-09-22T23:05:52Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/devcontainers/features/issues/952", + "title": "invalid mount config for type \"bind\": bind source path does not exist: /var/run/docker.sock", + "repo": "devcontainers/features", + "created": "2024-04-17T18:00:03Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/MCSManager/MCSManager/issues/2230", + "title": "[Bug Report] Docker\u90e8\u7f72MCSM\uff0c\u65e0\u6cd5\u7528\u5bb9\u5668\u5316\u8fd0\u884c\u5e7b\u517d\u5e15\u9c81", + "repo": "MCSManager/MCSManager", + "created": "2026-07-10T15:21:39Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/devcontainers/cli/issues/613", + "title": "Error response from daemon: invalid mount config for type \"bind\": bind source path does not exist", + "repo": "devcontainers/cli", + "created": "2023-08-08T21:40:32Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/common-workflow-language/cwltool/issues/1433", + "title": "docker: Error response from daemon: invalid mount config for type \"bind\": bind source path does not exist", + "repo": "common-workflow-language/cwltool", + "created": "2021-04-08T13:50:05Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/IMAP-Science-Operations-Center/imap_L3_processing/issues/191", + "title": "BUG - GLOWS integration tests fail in CI", + "repo": "IMAP-Science-Operations-Center/imap_L3_processing", + "created": "2026-09-04T13:22:42Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/ResearchDataCom/actions/issues/10", + "title": "Testing fails when run via Act due to nested volume mounts", + "repo": "ResearchDataCom/actions", + "created": "2026-07-30T15:44:39Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/zdeneksvarc/postal-tls/issues/3", + "title": "Error response from daemon: invalid mount config for type \"bind\": bind source path does not exist: /opt/postal-tls/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.dom/mail.dom.crt", + "repo": "zdeneksvarc/postal-tls", + "created": "2023-04-27T13:29:27Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/docker/awesome-compose/issues/412", + "title": "[fix] Error response from daemon: invalid mount config for type \"bind\": bind source path does not exist: /home/traefik/dockerWork/workpress/wordpress", + "repo": "docker/awesome-compose", + "created": "2024-01-15T02:38:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/harbor-framework/harbor/issues/2097", + "title": "\u3010Bug\u3011Docker fails to identiry the local directory", + "repo": "harbor-framework/harbor", + "created": "2026-06-25T14:36:48Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/ospaarmann/exdgraph/issues/30", + "title": "Cannot create container for service", + "repo": "ospaarmann/exdgraph", + "created": "2022-04-20T07:56:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DefuzeX-AI/AgentBehaviorBench/issues/116", + "title": "[runtime] Docker interceptor temp paths are not mountable from Colima on macOS", + "repo": "DefuzeX-AI/AgentBehaviorBench", + "created": "2026-09-20T16:48:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/cli/issues/7176", + "title": "Truncating log lines at 75 characters makes them useless.", + "repo": "docker/cli", + "created": "2026-08-11T16:53:51Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/arifulhb/laravel-docker-app/issues/2", + "title": "Error on Docker-compose up...can't find directory for Volume Mount", + "repo": "arifulhb/laravel-docker-app", + "created": "2021-12-17T19:31:28Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/circlefin/arc-node/issues/51", + "title": "Main issue with prometheus metrics endpoint ports", + "repo": "circlefin/arc-node", + "created": "2026-05-01T03:37:33Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/fadhilkurnia/xdn/issues/82", + "title": "primary-backup service launch livelocks: duplicate pb-init threads race the recorder's bind-mount wipe", + "repo": "fadhilkurnia/xdn", + "created": "2026-08-20T06:31:28Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/eclipse-enclave/enclave/issues/41", + "title": "Deleting disposable cache data can break Enclave sessions", + "repo": "eclipse-enclave/enclave", + "created": "2026-08-10T23:47:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/enclaver-io/enclaver/issues/241", + "title": "Docker bind error: : invalid mount config for type \"bind\"", + "repo": "enclaver-io/enclaver", + "created": "2025-11-08T00:17:48Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/bytedance/deer-flow/issues/2207", + "title": "\u5728win\u7cfb\u7edfwsl\u4e2d\u4f7f\u7528make docker-start\u542f\u52a8\uff0c\u6c99\u7bb1\u5bb9\u5668\u65e0\u6cd5\u6b63\u5e38\u6302\u8f7d\uff0c\u5bfc\u81f4\u6280\u80fd\u65e0\u6cd5\u4f7f\u7528", + "repo": "bytedance/deer-flow", + "created": "2026-04-14T02:54:22Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/shonansurvivors/laravel-fargate-app/issues/27", + "title": "4.23\u3000\u30ef\u30fc\u30af\u30d5\u30ed\u30fc\u306e\u5b9f\u884c", + "repo": "shonansurvivors/laravel-fargate-app", + "created": "2022-11-08T07:25:23Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/hubblo-org/publications/issues/1", + "title": "Erreur ", + "repo": "hubblo-org/publications", + "created": "2021-11-04T14:27:07Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/devcontainers/spec/issues/335", + "title": "Proposal: Predefined variables for user's home directory", + "repo": "devcontainers/spec", + "created": "2023-11-07T23:53:58Z", + "state": "open", + "comments": 21, + "verified": true + }, + { + "url": "https://github.com/MCSManager/MCSManager/issues/2198", + "title": "[Bug Report] \u5728 daemon \u5bb9\u5668\u4e2d\u4ee5\u975e root \u6743\u9650\u8fd0\u884c daemon \uff0c\u542f\u52a8 docker \u5b9e\u4f8b\u65f6\uff0c\u6302\u8f7d\u66f4\u591a\u76ee\u5f55\u4f1a\u51fa\u73b0\u521b\u5efa\u6587\u4ef6\u5939\u5931\u8d25\u7684\u95ee\u9898", + "repo": "MCSManager/MCSManager", + "created": "2026-06-05T01:37:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tabaresjc/HeadUP/issues/25", + "title": "Cannot run the docker...", + "repo": "tabaresjc/HeadUP", + "created": "2021-01-05T11:19:26Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 544 +} \ No newline at end of file diff --git a/bench/data/build--buildkit-required.json b/bench/data/build--buildkit-required.json new file mode 100644 index 0000000..40573b1 --- /dev/null +++ b/bench/data/build--buildkit-required.json @@ -0,0 +1,280 @@ +{ + "category": "build: BuildKit required", + "rule": "build.buildkit", + "query": "\"requires BuildKit\" is:issue created:>=2021-01-01", + "total": 443, + "sample": [ + { + "url": "https://github.com/gonuit/heroku-docker-deploy/issues/16", + "title": "Docker requires BuildKit", + "repo": "gonuit/heroku-docker-deploy", + "created": "2021-10-03T05:55:30Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/crazy-goat/go-mesi/issues/442", + "title": "[tests] servers/test-server image unbuildable without the buildx component \u2014 COPY --chmod requires BuildKit (docker build, DOCKER_BUILDKIT=1 and docker compose all fail locally)", + "repo": "crazy-goat/go-mesi", + "created": "2026-09-22T11:49:33Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/compose/issues/13695", + "title": "[BUG] Confusing error messages when Bake mode requires BuildKit but it's not enabled", + "repo": "docker/compose", + "created": "2026-04-01T07:18:21Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/devspace-sh/devspace/issues/3293", + "title": "Support buildkit backend for local builds", + "repo": "devspace-sh/devspace", + "created": "2026-08-10T09:09:03Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/airvzxf/ftp-deployment-action/issues/210", + "title": "[EPIC] Dockerfile/Makefile hardening audit (F2 round)", + "repo": "airvzxf/ftp-deployment-action", + "created": "2026-09-03T20:31:37Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/NVIDIA/NemoClaw/issues/9921", + "title": "Hermes Portable image build uses a BuildKit-only COPY option", + "repo": "NVIDIA/NemoClaw", + "created": "2026-08-21T20:02:18Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/airvzxf/ftp-deployment-action/issues/222", + "title": "[EPIC] workflows hardening audit (F2 round)", + "repo": "airvzxf/ftp-deployment-action", + "created": "2026-09-03T20:32:55Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ScottRBK/eval-harness/issues/5", + "title": "Build eval-owned Docker images with BuildKit", + "repo": "ScottRBK/eval-harness", + "created": "2026-08-09T09:13:23Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dromara/Jpom/issues/49", + "title": "\u4fee\u6539docker demon\u914d\u7f6e\u540e\uff0cdocker\u6784\u5efa\u6ca1\u6709\u4f7f\u7528\u65b0\u914d\u7f6e", + "repo": "dromara/Jpom", + "created": "2023-06-14T12:53:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/NVIDIA/workbench-example-onboarding-project/issues/21", + "title": "Building JupyterLab failed", + "repo": "NVIDIA/workbench-example-onboarding-project", + "created": "2025-10-15T12:24:49Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/portainer/portainer/issues/10482", + "title": "Build Image throws BuildKit error for --chmod", + "repo": "portainer/portainer", + "created": "2023-10-16T11:31:27Z", + "state": "open", + "comments": 12, + "verified": true + }, + { + "url": "https://github.com/NVIDIA/NemoClaw/issues/10007", + "title": "Hermes Portable Dockerfile requires BuildKit-only features on Podman", + "repo": "NVIDIA/NemoClaw", + "created": "2026-08-23T15:18:33Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/airvzxf/ftp-deployment-action/issues/156", + "title": "test(integration): Dockerfile.test-server requires BuildKit but Makefile uses plain 'docker build'", + "repo": "airvzxf/ftp-deployment-action", + "created": "2026-09-03T16:42:32Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/nuriel77/goshimmer-playbook/issues/29", + "title": "Playbook failed while installing Docker image", + "repo": "nuriel77/goshimmer-playbook", + "created": "2021-10-01T12:49:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Mollomm1/NovetusDocker/issues/2", + "title": "container fails to build at step 7/13", + "repo": "Mollomm1/NovetusDocker", + "created": "2025-07-01T18:26:16Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/bmuschko/gradle-docker-plugin/issues/1226", + "title": "RUN --mount=type=bind doesn't work", + "repo": "bmuschko/gradle-docker-plugin", + "created": "2024-11-04T16:38:40Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/night1008/night1008.github.io/issues/71", + "title": "mac + colima + docker-cli \u4e0b --mount build \u5931\u8d25", + "repo": "night1008/night1008.github.io", + "created": "2026-04-21T02:54:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Radiergummi/cetacean/issues/89", + "title": "Add windows nanoserver container image", + "repo": "Radiergummi/cetacean", + "created": "2026-07-01T16:30:51Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/shakacode/react-on-rails-demo-marketplace-rsc/issues/201", + "title": "Follow-up: .controlplane/.dockerignore is never read by docker build", + "repo": "shakacode/react-on-rails-demo-marketplace-rsc", + "created": "2026-09-02T09:52:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/caretdev/iris-python-faker/issues/1", + "title": "Installation error in WSL2", + "repo": "caretdev/iris-python-faker", + "created": "2022-01-08T15:42:17Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Azure/acr/issues/721", + "title": "Allow `buildkit` build in `az acr build`", + "repo": "Azure/acr", + "created": "2023-09-11T07:28:48Z", + "state": "open", + "comments": 14, + "verified": true + }, + { + "url": "https://github.com/jojo243/seafile-docker-arm64/issues/19", + "title": "Buildkit Error", + "repo": "jojo243/seafile-docker-arm64", + "created": "2022-01-05T15:57:36Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ptone/scion/issues/1453", + "title": "fix(build): enable BuildKit in cloudbuild-omni.yaml for --mount support", + "repo": "ptone/scion", + "created": "2026-09-07T12:00:19Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dotnet/dotnet-docker/issues/5654", + "title": "Consider using Docker custom build outputs (`--output`) for generated Dockerfiles and Readmes", + "repo": "dotnet/dotnet-docker", + "created": "2024-07-05T19:00:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/o1-labs/submission-updater/issues/6", + "title": "Restructure the dual-binary Dockerfile: variant stages + directory COPY", + "repo": "o1-labs/submission-updater", + "created": "2026-08-31T15:35:12Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/moby/moby/issues/46727", + "title": "`ADD --checksum` doesn't work with windows builds", + "repo": "moby/moby", + "created": "2023-10-26T08:06:35Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/paulkokos/project-management-dashboard/issues/93", + "title": "Keyboard shortcuts system", + "repo": "paulkokos/project-management-dashboard", + "created": "2025-11-05T03:49:58Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/dotnet/Docker.DotNet/issues/635", + "title": "Docket.DotNet Build Image has undocumented limitations", + "repo": "dotnet/Docker.DotNet", + "created": "2023-05-13T21:13:22Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/fabric8io/docker-maven-plugin/issues/1711", + "title": "Failed to build the docker image on using \"chmod\" option", + "repo": "fabric8io/docker-maven-plugin", + "created": "2023-10-05T18:14:50Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/calpaterson/csvbase/issues/126", + "title": "Will this run on an ARM64 Raspberry Pi?", + "repo": "calpaterson/csvbase", + "created": "2024-06-02T15:20:23Z", + "state": "open", + "comments": 4, + "verified": true + } + ], + "precision": 0.9666666666666667, + "adjusted": 428 +} \ No newline at end of file diff --git a/bench/data/build--copy-failed-legacy.json b/bench/data/build--copy-failed-legacy.json new file mode 100644 index 0000000..7ecf8a3 --- /dev/null +++ b/bench/data/build--copy-failed-legacy.json @@ -0,0 +1,280 @@ +{ + "category": "build: COPY failed legacy", + "rule": "build.context", + "query": "\"COPY failed: file not found in build context\" is:issue created:>=2021-01-01", + "total": 417, + "sample": [ + { + "url": "https://github.com/NekomyaDev/nudge/issues/38", + "title": "Dockerfile build fails: COPY references non-existent runtime/ directory", + "repo": "NekomyaDev/nudge", + "created": "2026-09-23T00:31:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kubespace/kubespace/issues/1", + "title": "COPY failed: file not found in build context or excluded by .dockerignore: stat kubespace: file does not exist", + "repo": "kubespace/kubespace", + "created": "2022-06-14T11:28:08Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/vsoch/cdb/issues/6", + "title": "COPY failed: file not found in build context or excluded by .dockerignore: stat data: file does not exist", + "repo": "vsoch/cdb", + "created": "2021-01-16T16:48:21Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/swarmpit/swarmpit/issues/651", + "title": "COPY failed: file not found in build context or excluded by .dockerignore: stat target/swarmpit.jar: file does not exist", + "repo": "swarmpit/swarmpit", + "created": "2023-01-18T14:34:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tfeldmann/organize/issues/409", + "title": "Dockerfile Image Creation Error : COPY failed: file not found in build context or excluded by .dockerignore: stat organize: file does not exist", + "repo": "tfeldmann/organize", + "created": "2024-08-23T10:32:02Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/rancher/rancher/issues/51500", + "title": "[BUG]COPY failed: file not found in build context or excluded by .dockerignore: stat root/vv/rancher/hack/airgap/go.mod: file does not exist exit status 1", + "repo": "rancher/rancher", + "created": "2025-08-15T03:29:34Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/strandborg/AlsavoCtrl/issues/22", + "title": "Error during docker build - COPY failed: file not found in build context or excluded by .dockerignore: stat alsavo.py: file does not exist", + "repo": "strandborg/AlsavoCtrl", + "created": "2024-01-04T00:45:40Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/eth0izzle/shhgit/issues/101", + "title": "Error docker install \"docker-compose up\"", + "repo": "eth0izzle/shhgit", + "created": "2022-04-30T00:11:47Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/microsoft-foundry/foundry-samples/issues/626", + "title": "Bug: Docker build fails due to missing read_cosmos_data.py in migration directory", + "repo": "microsoft-foundry/foundry-samples", + "created": "2026-03-26T07:09:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/rohitbakoliya/sieve.ai/issues/11", + "title": "\"uploads\" package is missing", + "repo": "rohitbakoliya/sieve.ai", + "created": "2024-01-22T13:30:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/aertslab/scenicplus/issues/604", + "title": "Dockerfile does not build", + "repo": "aertslab/scenicplus", + "created": "2025-09-09T18:51:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/aertslab/scenicplus/issues/213", + "title": "Dockerfile does not build ", + "repo": "aertslab/scenicplus", + "created": "2023-08-24T16:56:12Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/wpdiaries/wordpress-xdebug/issues/2", + "title": "COPY command keeps failing", + "repo": "wpdiaries/wordpress-xdebug", + "created": "2023-11-17T16:32:12Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/IfCodingWereNatural/minimal-nvim/issues/1", + "title": ".cache directory not exist", + "repo": "IfCodingWereNatural/minimal-nvim", + "created": "2023-08-19T13:56:07Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/ChristopherSweeney/RGBD_SIM/issues/1", + "title": "Dockerfile fails because drake_install does not exist", + "repo": "ChristopherSweeney/RGBD_SIM", + "created": "2021-11-15T23:57:48Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/NeodomoEwe/hassio-hotspot/issues/4", + "title": "Cannot install: stat run.sh: file does not exist", + "repo": "NeodomoEwe/hassio-hotspot", + "created": "2023-01-25T21:01:17Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/BeardedTek-com/rtsp2mjpg/issues/2", + "title": "COPY failed (COPY etc to /etc)", + "repo": "BeardedTek-com/rtsp2mjpg", + "created": "2026-02-09T20:22:57Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/CycloneDX/cyclonedx-cli/issues/259", + "title": "Dockerfile Problem", + "repo": "CycloneDX/cyclonedx-cli", + "created": "2022-08-17T03:27:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/accreleus/quasar-images/issues/31", + "title": "benchapp template build fails under the control-plane's dir-scoped context: COPY images/quasar-benchapp/quasar-benchapp expects repo-root context", + "repo": "accreleus/quasar-images", + "created": "2026-08-27T13:03:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RickStrahl/AlbumViewerVNext/issues/17", + "title": "docker-compose fails", + "repo": "RickStrahl/AlbumViewerVNext", + "created": "2023-07-05T09:40:54Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/sberk42/fritzbox_exporter/issues/23", + "title": "Create from Dockerfile - metrics.json / metrics-lua.json cannot be copied", + "repo": "sberk42/fritzbox_exporter", + "created": "2023-01-15T13:02:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/bigornoo/pcloud_docker/issues/1", + "title": "copy failed", + "repo": "bigornoo/pcloud_docker", + "created": "2023-01-12T12:26:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/microsoft/navcontainerhelper/issues/4210", + "title": "[Bug]: Flush-ContainerHelperCache deletes the empty \"my\" folder out of a concurrent New-BcImage build context, breaking parallel builds on a shared host", + "repo": "microsoft/navcontainerhelper", + "created": "2026-09-18T05:19:13Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/NeodomoEwe/hassio-hotspot/issues/5", + "title": "Dockerfile problem on upgrading", + "repo": "NeodomoEwe/hassio-hotspot", + "created": "2023-02-06T01:44:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/IntelSmartEdge/open-developer-experience-kits/issues/1", + "title": "New Release setup failed due to file does not exist", + "repo": "IntelSmartEdge/open-developer-experience-kits", + "created": "2021-10-13T02:59:30Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/ProyectoPAIS/VICOS/issues/2", + "title": "Error building docker image", + "repo": "ProyectoPAIS/VICOS", + "created": "2023-02-09T11:42:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/guojiaxing1995/easy-test/issues/51", + "title": "docker\u6784\u5efa\u62a5\u9519", + "repo": "guojiaxing1995/easy-test", + "created": "2024-05-10T12:12:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/google/ax/issues/364", + "title": "`make build-task-runner` fails: `.dockerignore` excludes `bin/`, which `Dockerfile.task-runner` COPYs from", + "repo": "google/ax", + "created": "2026-09-22T08:40:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/mSorok/SugarRemovalWeb/issues/1", + "title": "docker-compose build fails atm, dependency issue?", + "repo": "mSorok/SugarRemovalWeb", + "created": "2022-08-11T20:19:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/linera-io/linera-protocol/issues/2873", + "title": "issue while docker build", + "repo": "linera-io/linera-protocol", + "created": "2024-11-12T18:29:23Z", + "state": "open", + "comments": 3, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 417 +} \ No newline at end of file diff --git a/bench/data/build--copy-source-not-found.json b/bench/data/build--copy-source-not-found.json new file mode 100644 index 0000000..a0de08e --- /dev/null +++ b/bench/data/build--copy-source-not-found.json @@ -0,0 +1,280 @@ +{ + "category": "build: COPY source not found", + "rule": "build.context", + "query": "\"failed to compute cache key\" \"not found\" is:issue created:>=2021-01-01", + "total": 1421, + "sample": [ + { + "url": "https://github.com/earthly/earthly/issues/1614", + "title": " failed to compute cache key", + "repo": "earthly/earthly", + "created": "2022-01-26T20:51:07Z", + "state": "open", + "comments": 15, + "verified": true + }, + { + "url": "https://github.com/InterviewReady/joy-of-system-design/issues/5", + "title": "failed to solve: failed to compute cache key: \"/yarn.lock\" not found: not found", + "repo": "InterviewReady/joy-of-system-design", + "created": "2023-01-19T08:54:32Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/allofphysicsgraph/ui_v8_website_flask_neo4j/issues/204", + "title": "github action to build container fails due to files needed by Dockerfile but not in git repo", + "repo": "allofphysicsgraph/ui_v8_website_flask_neo4j", + "created": "2026-09-12T17:31:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/loblab/noip-renew/issues/79", + "title": "failed to compute cache key: \"/noip-renew.py\" not found: not found", + "repo": "loblab/noip-renew", + "created": "2021-08-10T20:42:01Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/spliit-app/spliit/issues/235", + "title": "ERROR: failed to solve: failed to compute cache key: \"/next.config.js\": not found", + "repo": "spliit-app/spliit", + "created": "2024-10-03T05:47:08Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/sagostin/gofaxserver/issues/3", + "title": "Dockerfile can't build: COPY go.mod go.sum fails, repo has no go.sum (vendored deps)", + "repo": "sagostin/gofaxserver", + "created": "2026-08-31T22:39:26Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/edusense/edusense/issues/42", + "title": "failed to compute cache key: \"/app/build/examples/edusense/edusense.bin\" not found: not found", + "repo": "edusense/edusense", + "created": "2023-02-03T02:40:02Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/moby/buildkit/issues/4128", + "title": "failed to solve: failed to compute cache key: failed to calculate checksum of ref", + "repo": "moby/buildkit", + "created": "2023-08-09T08:06:59Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/guge199205-byte/QuantMind-oss/issues/3", + "title": "docker compose up \u542f\u52a8\u5931\u8d25", + "repo": "guge199205-byte/QuantMind-oss", + "created": "2026-08-05T10:06:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/weka/export/issues/53", + "title": "Docker build fails with the following: failed to compute cache key: \"/tarball/export/export\" not found: not found", + "repo": "weka/export", + "created": "2023-06-07T19:33:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/analogdevicesinc/microcontroller-hackathon/issues/1", + "title": "Docker container build fail", + "repo": "analogdevicesinc/microcontroller-hackathon", + "created": "2026-08-07T09:13:34Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/PlaiPin/rosclaw/issues/3", + "title": "Packages not found", + "repo": "PlaiPin/rosclaw", + "created": "2026-03-03T06:55:22Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/vuongnguyenbinh/ZaloCRM/issues/4", + "title": "\"/app/backend/prisma.config.ts\": not found", + "repo": "vuongnguyenbinh/ZaloCRM", + "created": "2026-03-28T15:45:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/openremote/openremote/issues/3025", + "title": "Manager Docker build sometimes fails to compute cache key", + "repo": "openremote/openremote", + "created": "2026-07-14T14:47:22Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/fyrlove/r0re/issues/1", + "title": "The Docker build fails because container-android/AGENTS.md is missing", + "repo": "fyrlove/r0re", + "created": "2026-08-11T10:42:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Vynx-Velvet/Flyx-main/issues/20", + "title": "/app/pow_wasm.js missing", + "repo": "Vynx-Velvet/Flyx-main", + "created": "2026-07-19T18:33:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/crossplane-contrib/provider-palette/issues/115", + "title": "Docker image build fails: terraform-provider-spectrocloud zip not found in build context (linux_arm64)", + "repo": "crossplane-contrib/provider-palette", + "created": "2026-09-01T07:22:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/coleschaffer/Gabagool/issues/3", + "title": "failed to solve: failed to compute cache key: failed to calculate checksum of ref 686290d4-7997-43f4-8fd3-a67552da50d3::78wd65fbco1b74i7qffzq6jhr: \"/app/public\": not found", + "repo": "coleschaffer/Gabagool", + "created": "2025-12-26T06:25:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/pyflink/playgrounds/issues/22", + "title": "failed to compute cache key: \"/flink-sql-connector-kafka_2.11-1.13.0.jar\" not found: not found", + "repo": "pyflink/playgrounds", + "created": "2022-06-16T07:03:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/anmonteiro/aws-lambda-ocaml-runtime/issues/70", + "title": "How to resolve failed to compute cache key: \"/esy.lock\" not found: not found error when running build.sh", + "repo": "anmonteiro/aws-lambda-ocaml-runtime", + "created": "2023-04-23T22:55:44Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Hochfrequenz/ahb-tabellen/issues/395", + "title": "docs are oudated?: cannot start local server: failed to compute cache key / azure-mock not found", + "repo": "Hochfrequenz/ahb-tabellen", + "created": "2025-01-25T09:31:31Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/ClawixAI/clawix/issues/58", + "title": "**Bug: `egress-firewall.sh` path mismatch in python-runner Dockerfile**", + "repo": "ClawixAI/clawix", + "created": "2026-08-08T06:42:08Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/onyx-dot-app/onyx/issues/14177", + "title": "backend/Dockerfile: chown -R onyx:onyx /app runs before /app exists (introduced in #6390), fails every fresh build", + "repo": "onyx-dot-app/onyx", + "created": "2026-08-23T08:37:36Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/kieranroneill/q-rewrite/issues/9", + "title": "Feedback Julian", + "repo": "kieranroneill/q-rewrite", + "created": "2026-08-21T09:38:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/paolorechia/open-code-gen-api/issues/1", + "title": "Models not found", + "repo": "paolorechia/open-code-gen-api", + "created": "2023-01-09T10:48:04Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Aaronhrndz/antigravity-cli-addon/issues/2", + "title": "Build fails for version 1.8.8 on aarch64: \"/run_agy.sh\": not found", + "repo": "Aaronhrndz/antigravity-cli-addon", + "created": "2026-06-19T10:03:09Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/VisionRush/DeepFakeDefenders/issues/4", + "title": "ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref 338532d9-772c-451b-ba3d-c9fd060590f3::bivmsdw8vm9wgmzzrffunihup: \"/pre_model\": not found", + "repo": "VisionRush/DeepFakeDefenders", + "created": "2024-09-04T02:05:33Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/GitDakky/mitsubishi-ae200-hass/issues/2", + "title": "Installation Failure issue: \"/pymelans\": not found on Home Assistant OS and kills integration", + "repo": "GitDakky/mitsubishi-ae200-hass", + "created": "2026-07-17T01:34:20Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Goblin-Airdrops/allora-coin-prediction-node-goblin/issues/1", + "title": ".pth model ERROR not found", + "repo": "Goblin-Airdrops/allora-coin-prediction-node-goblin", + "created": "2024-09-22T18:32:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/biobricks-ai/toxtransformer/issues/3", + "title": "selfies_property_val_tokenizer not found", + "repo": "biobricks-ai/toxtransformer", + "created": "2025-10-14T07:52:40Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 1421 +} \ No newline at end of file diff --git a/bench/data/build--outside-build-context.json b/bench/data/build--outside-build-context.json new file mode 100644 index 0000000..04c230a --- /dev/null +++ b/bench/data/build--outside-build-context.json @@ -0,0 +1,280 @@ +{ + "category": "build: outside build context", + "rule": "build.context", + "query": "\"forbidden path outside the build context\" is:issue created:>=2021-01-01", + "total": 65, + "sample": [ + { + "url": "https://github.com/jtfrey/xdmod-container/issues/3", + "title": "COPY failed: forbidden path outside the build context: XDMoD-start", + "repo": "jtfrey/xdmod-container", + "created": "2021-09-22T11:30:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/aws-samples/iot-network-traffic-control-and-load-testing-simulator/issues/6", + "title": "npx cdk deploy IoTFaultInjectionSimulatorStack", + "repo": "aws-samples/iot-network-traffic-control-and-load-testing-simulator", + "created": "2023-06-27T11:29:13Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/PacktPublishing/Event-Driven-Architecture-in-Golang/issues/9", + "title": "Docker build fails", + "repo": "PacktPublishing/Event-Driven-Architecture-in-Golang", + "created": "2023-08-23T05:01:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/myfjdthink/doris-docker-compose/issues/1", + "title": "which version of docker-compose and docker used?", + "repo": "myfjdthink/doris-docker-compose", + "created": "2022-08-15T13:41:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/intuinno/conceptvector/issues/25", + "title": "docker-compose up throws Error", + "repo": "intuinno/conceptvector", + "created": "2021-09-27T12:25:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Algorithmic-Battle/algobattle/issues/168", + "title": "Allow for shared implementations", + "repo": "Algorithmic-Battle/algobattle", + "created": "2024-10-19T21:21:49Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/davglass/bambu-farm/issues/4", + "title": "Cannot even start it", + "repo": "davglass/bambu-farm", + "created": "2024-03-27T15:11:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/GoogleContainerTools/skaffold/issues/7504", + "title": "build can not use dockerfiles out of the docker context", + "repo": "GoogleContainerTools/skaffold", + "created": "2022-06-05T01:35:46Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/AxonIQ/axon-observability-demo/issues/1", + "title": "docker-compose build FAILED", + "repo": "AxonIQ/axon-observability-demo", + "created": "2022-11-09T13:59:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/david-svitov/HAHA/issues/2", + "title": "Edits needed to the Dockerfile", + "repo": "david-svitov/HAHA", + "created": "2024-04-05T00:14:46Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/hashicorp/waypoint/issues/2711", + "title": "Docker build: Fails if both 'dockerfile' and 'context' point outside of current dir tree", + "repo": "hashicorp/waypoint", + "created": "2021-11-10T21:20:28Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/moby/buildkit/issues/2130", + "title": "BuildKit should provide clear error if files are referenced outside build context", + "repo": "moby/buildkit", + "created": "2021-05-21T15:50:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/moby/buildkit/issues/2131", + "title": "BuildKit uses wrong path when referencing files outside of build context", + "repo": "moby/buildkit", + "created": "2021-05-21T15:52:16Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/tjmlabs/AgentRun/issues/20", + "title": "not executing the python code ; resulting in internal server error", + "repo": "tjmlabs/AgentRun", + "created": "2025-02-05T02:35:27Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/OktopUSP/oktopus/issues/379", + "title": "Build of containers fails due to 'forbidden path outside the build context'", + "repo": "OktopUSP/oktopus", + "created": "2025-10-28T08:59:21Z", + "state": "closed", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/openkruise/agents/issues/269", + "title": "[BUG] Deploying and building images on the ARM environment failed.", + "repo": "openkruise/agents", + "created": "2026-04-13T07:06:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/constructorfabric/gears-rust/issues/4798", + "title": "Docker: every build-from-source image is broken and make e2e-docker cannot run (no CI job builds images)", + "repo": "constructorfabric/gears-rust", + "created": "2026-09-14T10:57:58Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/typefly/TypeFly/issues/8", + "title": "COPY failed: forbidden path outside the build context: ../.. ()", + "repo": "typefly/TypeFly", + "created": "2025-03-20T11:58:03Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/SchneiderDaniel/cheasee-pi/issues/1246", + "title": "[Bug] Init extractor writes compose files to workdir root, not docker/ \u2014 inconsistent with docker/.env and run-pi.sh", + "repo": "SchneiderDaniel/cheasee-pi", + "created": "2026-07-14T15:15:03Z", + "state": "closed", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/MassonNN/masson-aiogram-template/issues/18", + "title": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0441 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043e\u043c \u043f\u0440\u0438 \u0431\u0438\u043b\u0434\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 (forbidden path outside the build context: ../poetry.lock ())", + "repo": "MassonNN/masson-aiogram-template", + "created": "2024-01-22T13:31:51Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/APTRS/APTRS/issues/55", + "title": "Docker compose fails at Step 9 in Dockerfile_API ", + "repo": "APTRS/APTRS", + "created": "2024-12-24T19:22:28Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/docs/issues/21827", + "title": "ADD/COPY do not strip paths outside of the build context", + "repo": "docker/docs", + "created": "2025-01-15T19:42:51Z", + "state": "closed", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/spider-rs/headless-browser/issues/7", + "title": "Docker build", + "repo": "spider-rs/headless-browser", + "created": "2025-05-21T13:32:35Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/colanode/colanode/issues/32", + "title": "Error running docker-compose", + "repo": "colanode/colanode", + "created": "2025-05-07T06:38:39Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/immich-app/immich/issues/90", + "title": "Build does not work", + "repo": "immich-app/immich", + "created": "2022-03-29T21:50:34Z", + "state": "closed", + "comments": 27, + "verified": true + }, + { + "url": "https://github.com/jupyterlab/jupyterlab/issues/15709", + "title": "Docker run fails to build image", + "repo": "jupyterlab/jupyterlab", + "created": "2024-01-29T00:24:44Z", + "state": "closed", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/The-Alpha-Project/alpha-core/issues/1460", + "title": "COPY failed: forbidden path outside the build context: ../../../../requirements.txt ()", + "repo": "The-Alpha-Project/alpha-core", + "created": "2024-11-13T02:56:10Z", + "state": "closed", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/opea-project/GenAIExamples/issues/153", + "title": "ChatQnA Megaservice container build issue", + "repo": "opea-project/GenAIExamples", + "created": "2024-05-17T14:15:57Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/go-nunu/nunu/issues/60", + "title": "\u8bf7\u95eemac\u7535\u8111\uff0c\u8fd0\u884c/projectName/deploy/build/Dockerfile\u7684\u811a\u672c\u6253\u955c\u50cf\uff0c\u8001\u662f\u62a5 COPY failed: forbidden path outside the build context: .. ()", + "repo": "go-nunu/nunu", + "created": "2024-06-03T13:58:10Z", + "state": "closed", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/SWE-agent/SWE-agent/issues/68", + "title": "Building eval docker failed: COPY failed: forbidden path outside the build context: ../evaluation/evaluation.py ()", + "repo": "SWE-agent/SWE-agent", + "created": "2024-04-05T09:08:23Z", + "state": "closed", + "comments": 3, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 65 +} \ No newline at end of file diff --git a/bench/data/compose--required-variable.json b/bench/data/compose--required-variable.json new file mode 100644 index 0000000..ed67241 --- /dev/null +++ b/bench/data/compose--required-variable.json @@ -0,0 +1,280 @@ +{ + "category": "compose: required variable", + "rule": "compose.env", + "query": "\"required variable\" \"is missing a value\" is:issue created:>=2021-01-01", + "total": 475, + "sample": [ + { + "url": "https://github.com/rubentalstra/FerroCHART/issues/213", + "title": "README quick start fails: the compose file requires FERROCHART_CDR_URL and FERROCHART_TERM_URL even with --profile demo", + "repo": "rubentalstra/FerroCHART", + "created": "2026-09-12T19:34:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Kralizek/octosmith/issues/49", + "title": "Validate required variables and secrets in the active execution context", + "repo": "Kralizek/octosmith", + "created": "2026-09-23T14:48:45Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/geekau/mediastack/issues/49", + "title": ".restart.sh: variable VPN_USERNAME is missing a value: err", + "repo": "geekau/mediastack", + "created": "2025-07-23T16:09:08Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/compose-spec/compose-go/issues/934", + "title": "`ExtractVariables` does not report a variable used inside a `${VAR:?\u2026}` message", + "repo": "compose-spec/compose-go", + "created": "2026-09-23T11:50:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/42ae/moodle-docker/issues/12", + "title": "`compose.yml` is not compatible with `_FILE` environment variables", + "repo": "42ae/moodle-docker", + "created": "2026-09-16T15:44:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/KeniBeck/MinecraftBedrock/issues/5", + "title": "La docuemntacion de la instalacion es incorecta", + "repo": "KeniBeck/MinecraftBedrock", + "created": "2026-08-20T04:29:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/optimizr-tech/optimizr-actions/issues/265", + "title": ":wrench: feat(ci): let _docker-compose-validate accept compose env for required vars", + "repo": "optimizr-tech/optimizr-actions", + "created": "2026-09-20T05:47:27Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/TAIPANBOX/stack-single/issues/62", + "title": "install.sh gives .env no backup path, and a re-run with .env gone regenerates a Postgres password pgdata does not know", + "repo": "TAIPANBOX/stack-single", + "created": "2026-09-18T13:44:54Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Osmantic/ODS/issues/5633", + "title": "test-bootstrap-mode.sh reports 'Invalid compose configuration' on any clean checkout", + "repo": "Osmantic/ODS", + "created": "2026-09-17T10:59:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/frappe/crm/issues/2230", + "title": "SITES_RULE Missing on Production install", + "repo": "frappe/crm", + "created": "2026-05-28T01:22:37Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/menna-nageh/retroloop-/issues/6", + "title": "Configure environment variables", + "repo": "menna-nageh/retroloop-", + "created": "2026-09-06T13:43:01Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/MrEyratnz/software-factory/issues/344", + "title": "[tech-debt][medium] docs/observability.md:94 \u2014 operator docker compose logs/down commands omit --env-file and fail", + "repo": "MrEyratnz/software-factory", + "created": "2026-07-28T10:21:06Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/stellXpress/backend/issues/39", + "title": "Service starts successfully even with missing required environment variables", + "repo": "stellXpress/backend", + "created": "2026-09-03T17:56:07Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/NuimanLP/srisurart-pos-flutter/issues/2", + "title": "Backend phase 1 \u2014 program brief (NestJS multi-tenant server, Architecture A + T1)", + "repo": "NuimanLP/srisurart-pos-flutter", + "created": "2026-09-04T12:16:52Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/StellarExpress/backend/issues/33", + "title": "Service starts successfully even with missing required environment variables", + "repo": "StellarExpress/backend", + "created": "2026-09-03T18:02:37Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/t59688/arboris-novel/issues/30", + "title": "\u660e\u660e\u8bbe\u7f6e\u4e86\u7684\u5b57\u6bb5\uff0c\u5374\u663e\u793a\u6ca1\u6709\u8bbe\u7f6e", + "repo": "t59688/arboris-novel", + "created": "2025-12-28T13:49:51Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/Antropophag/ic/issues/249", + "title": "podman-compose 1.6.0 \u043d\u0435 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442 --env-file \u043f\u0440\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 deployment contracts", + "repo": "Antropophag/ic", + "created": "2026-08-11T18:30:47Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/sakibsadmanshajib/hive/issues/1610", + "title": "post-deploy-verify is failing against the demo box", + "repo": "sakibsadmanshajib/hive", + "created": "2026-08-31T10:05:18Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/kreigan/adm-composectl/issues/2", + "title": "Having a common \".env\" file ignores stack's \".env\" file", + "repo": "kreigan/adm-composectl", + "created": "2026-01-04T20:17:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/matrixorigin/memoria/issues/18", + "title": "[Bug]: docker compose up -d error", + "repo": "matrixorigin/memoria", + "created": "2026-03-13T19:24:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/frappe/helpdesk/issues/3067", + "title": "Getting \"Generating Compose File failed\" error", + "repo": "frappe/helpdesk", + "created": "2026-02-26T06:38:13Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/nathanpond/Auton8/issues/519", + "title": "Guard hardening, round 6: the round-8 guards, including one #513 item closed unfixed", + "repo": "nathanpond/Auton8", + "created": "2026-09-17T15:52:37Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/cubrid-lab/cubrid-mcp-server/issues/177", + "title": "fix(config): reject empty required connection environment variables", + "repo": "cubrid-lab/cubrid-mcp-server", + "created": "2026-09-18T12:41:49Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/avnox-com/papermark-self-host/issues/1", + "title": ".env values not being picked up", + "repo": "avnox-com/papermark-self-host", + "created": "2025-11-06T13:51:37Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/project-chip/certification-tool/issues/986", + "title": "[Bug] Auto Install Error", + "repo": "project-chip/certification-tool", + "created": "2026-05-08T22:12:14Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/frappe/lms/issues/2130", + "title": "Docker images on ghcr.io are outdated (December 2025) \u2014 critical bug fixes not available in production deployments + easy-install.py bug", + "repo": "frappe/lms", + "created": "2026-02-25T09:56:49Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/docker/cli/issues/7313", + "title": "`docker stack` subcommands: the `:?` interpolation operator and hyphens do not mix", + "repo": "docker/cli", + "created": "2026-09-17T17:31:09Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/the-deep/deeper/issues/1004", + "title": "docker-compose failing", + "repo": "the-deep/deeper", + "created": "2024-10-21T14:57:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/BioMicroCenter/NExtSEEK/issues/112", + "title": "A $ in NEO4J_PASSWORD silently breaks compose, and the error blames .env for it", + "repo": "BioMicroCenter/NExtSEEK", + "created": "2026-08-20T20:41:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nathanpond/Auton8/issues/518", + "title": "Requiring AUTONATE_MOUNTS_ROOT broke preflight.sh run by hand and three skill docs", + "repo": "nathanpond/Auton8", + "created": "2026-09-17T15:52:10Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 0.8333333333333334, + "adjusted": 396 +} \ No newline at end of file diff --git a/bench/data/compose--unsupported-property.json b/bench/data/compose--unsupported-property.json new file mode 100644 index 0000000..1051047 --- /dev/null +++ b/bench/data/compose--unsupported-property.json @@ -0,0 +1,280 @@ +{ + "category": "compose: unsupported property", + "rule": "compose.version", + "query": "\"additional property\" \"is not allowed\" compose is:issue created:>=2021-01-01", + "total": 486, + "sample": [ + { + "url": "https://github.com/xebialabs-community/community-release-llm-integration/issues/10", + "title": "docker compose Additional property models is not allowed", + "repo": "xebialabs-community/community-release-llm-integration", + "created": "2026-03-10T08:39:29Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/langchain-ai/langchain-extract/issues/16", + "title": "Additional property name is not allowed", + "repo": "langchain-ai/langchain-extract", + "created": "2024-03-07T13:34:16Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/shahinism/docker-stealth-openvpn/issues/4", + "title": "Additional property openvpn is not allowed", + "repo": "shahinism/docker-stealth-openvpn", + "created": "2023-05-03T12:28:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tombstoneghost/TIWAP/issues/4", + "title": "Additional property app is not allowed", + "repo": "tombstoneghost/TIWAP", + "created": "2022-09-14T06:04:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/multica-ai/multica/issues/1401", + "title": "[Bug]: Installation fails with message: (root) Additional property name is not allowed", + "repo": "multica-ai/multica", + "created": "2026-04-20T16:28:49Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/dunglas/docker-private-composer-packages/issues/2", + "title": "OSX - build Additional property secrets is not allowed", + "repo": "dunglas/docker-private-composer-packages", + "created": "2022-09-20T22:26:08Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/UbikZ/docker-uo/issues/1", + "title": "(root) Additional property nginx is not allowed", + "repo": "UbikZ/docker-uo", + "created": "2021-09-11T03:40:35Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/containers/podman-compose/issues/1549", + "title": "Unclear error when `networks`/`volumes` are accidentally indented under `services`", + "repo": "containers/podman-compose", + "created": "2026-08-31T14:05:42Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/redTreeOnWall/memorains/issues/2", + "title": "services.reno_note_nginx Additional property proxy_pass is not allowed", + "repo": "redTreeOnWall/memorains", + "created": "2025-06-18T01:29:55Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/docker/multi-container-app/issues/22", + "title": "- ERROR: services.todo-app Additional property develop is not allowed", + "repo": "docker/multi-container-app", + "created": "2024-03-28T21:30:12Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/bigbluebutton/docker/issues/318", + "title": "Received Error 'services.webrtc-sfu.build Additional property additional_contexts is not allowed'", + "repo": "bigbluebutton/docker", + "created": "2024-02-15T16:57:08Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/matchory/docker-swarm-deployment-action/issues/183", + "title": "Support newer stack schemas for versionless Compose files", + "repo": "matchory/docker-swarm-deployment-action", + "created": "2026-08-04T14:03:22Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/ensdomains/ens-test-env/issues/12", + "title": "Running `docker compose up` does not run with error \"Additional property start_interval is not allowed\"", + "repo": "ensdomains/ens-test-env", + "created": "2025-10-02T17:09:02Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/docker/multi-container-app/issues/9", + "title": "Validating docker_tutorial/multi-container-app/compose.yaml: services.todo-app Additional property develop is not allowed", + "repo": "docker/multi-container-app", + "created": "2023-11-19T05:42:34Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/jcnewell/ergast-f1-api/issues/11", + "title": "Getting (root) Additional property ergast-db is not allowed when running docker-compose up --build -d --remove-orphans", + "repo": "jcnewell/ergast-f1-api", + "created": "2023-07-25T16:06:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/AbsaOSS/spline-getting-started/issues/47", + "title": "docker-compose up ERROR ", + "repo": "AbsaOSS/spline-getting-started", + "created": "2023-06-13T02:14:14Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/docker/multi-container-app/issues/11", + "title": "Docker compose up -d does not launch app, give error: '/multi-container-app/compose.yaml: services.todo-app Additional property develop is not allowed'", + "repo": "docker/multi-container-app", + "created": "2023-12-13T18:20:05Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/UKHomeOffice/docker-landscaper-server/issues/8", + "title": "docker compose build error", + "repo": "UKHomeOffice/docker-landscaper-server", + "created": "2024-12-05T16:30:28Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker-archive-public/docker.labs/issues/596", + "title": "wrong compose syntax", + "repo": "docker-archive-public/docker.labs", + "created": "2023-07-14T17:16:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/eip-work/kuboard-press/issues/602", + "title": "docker compose\u6267\u884c\u5931\u8d25", + "repo": "eip-work/kuboard-press", + "created": "2025-10-15T04:13:39Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/cer/event-sourcing-examples/issues/47", + "title": "Docker compose failure", + "repo": "cer/event-sourcing-examples", + "created": "2021-12-30T11:59:17Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/abhirockzz/oracle-nosql-jersey-dockercompose-sample/issues/1", + "title": "docker-compose up not working", + "repo": "abhirockzz/oracle-nosql-jersey-dockercompose-sample", + "created": "2023-01-31T14:15:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/codebygina/mediumish-theme-jekyll/issues/184", + "title": "Docker Compose file has invlaid syntax", + "repo": "codebygina/mediumish-theme-jekyll", + "created": "2021-09-16T20:39:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/knrew/decune/issues/406", + "title": "Compose v2.24.4\u5411\u3051integration gate\u3092\u8ffd\u52a0\u3057ports.name fixture\u3092\u5206\u96e2\u3059\u308b", + "repo": "knrew/decune", + "created": "2026-07-13T16:15:17Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/jmb12686/docker-multiarch-glusterfs-plugin/issues/1", + "title": "Error using compose file", + "repo": "jmb12686/docker-multiarch-glusterfs-plugin", + "created": "2021-03-06T00:09:21Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Juniper/open-nti/issues/279", + "title": "Error: \"Docker-Compose Make\"", + "repo": "Juniper/open-nti", + "created": "2022-09-13T23:00:18Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/askz/mikrotik-for-doprax/issues/1", + "title": "error at doprax ", + "repo": "askz/mikrotik-for-doprax", + "created": "2024-07-03T09:52:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DanielDent/docker-nginx-ssl-proxy/issues/35", + "title": "Documentation: fix docker-compose.yml", + "repo": "DanielDent/docker-nginx-ssl-proxy", + "created": "2022-10-06T05:19:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/cli/issues/4740", + "title": "docker stack up on compose with include not supported", + "repo": "docker/cli", + "created": "2023-12-23T02:14:31Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/docker/cli/issues/6542", + "title": "docker swarm - support for platform property in compose", + "repo": "docker/cli", + "created": "2025-10-08T05:41:14Z", + "state": "open", + "comments": 2, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 486 +} \ No newline at end of file diff --git a/bench/data/compose--variable-not-set.json b/bench/data/compose--variable-not-set.json new file mode 100644 index 0000000..4c9d1b5 --- /dev/null +++ b/bench/data/compose--variable-not-set.json @@ -0,0 +1,280 @@ +{ + "category": "compose: variable not set", + "rule": "compose.env", + "query": "\"variable is not set. Defaulting to a blank string\" is:issue created:>=2021-01-01", + "total": 2390, + "sample": [ + { + "url": "https://github.com/bausi2k/bmw-mqtt-bridge/issues/2", + "title": "WARN[0000] The \"NP\" variable is not set. Defaulting to a blank string.", + "repo": "bausi2k/bmw-mqtt-bridge", + "created": "2026-08-08T20:45:09Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/neurobagel/documentation/issues/421", + "title": "Cannot run local getting started recipe", + "repo": "neurobagel/documentation", + "created": "2026-08-11T19:31:25Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/ggml-org/llama.cpp/issues/28950", + "title": "Misc. bug: llama download -hf repo:model does not download mmproject", + "repo": "ggml-org/llama.cpp", + "created": "2026-09-15T12:55:39Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/shorin-nikita/lisa/issues/6", + "title": "[BUG] \u041e\u0448\u0438\u0431\u043a\u0430 1 \u043f\u0440\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 Supabase", + "repo": "shorin-nikita/lisa", + "created": "2026-08-16T15:37:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/NVIDIA-Omniverse-blueprints/digital-twins-for-fluid-simulation/issues/17", + "title": "Lite Mode : Still needs access to the NVCR login/ NGC _Key", + "repo": "NVIDIA-Omniverse-blueprints/digital-twins-for-fluid-simulation", + "created": "2026-08-09T08:41:06Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/firecrawl/firecrawl/issues/4137", + "title": "[Self-Host] Building with Podman instead of Docker", + "repo": "firecrawl/firecrawl", + "created": "2026-07-27T09:52:59Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/langflow-ai/openrag/issues/1411", + "title": "[Bug]: opensearch unhealthy", + "repo": "langflow-ai/openrag", + "created": "2026-04-16T02:21:32Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/YFGaia/dify-plus/issues/122", + "title": "setup bug", + "repo": "YFGaia/dify-plus", + "created": "2026-06-24T13:13:29Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/veerendra2/composeflux/issues/76", + "title": "Reconcilation failures", + "repo": "veerendra2/composeflux", + "created": "2026-09-10T17:08:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/hyperledger-archives/sawtooth-core/issues/2438", + "title": "The ISOLATION_ID variable is not set. Defaulting to a blank string.", + "repo": "hyperledger-archives/sawtooth-core", + "created": "2023-02-14T16:03:24Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/transmute-industries/sidetree.js/issues/217", + "title": "WARNING: The MNEMONIC variable is not set. Defaulting to a blank string.", + "repo": "transmute-industries/sidetree.js", + "created": "2021-08-10T11:34:37Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/rodrigovp/livro-kafka-connect/issues/1", + "title": "[BUG] - The \"PWD\" variable is not set. Defaulting to a blank string. ", + "repo": "rodrigovp/livro-kafka-connect", + "created": "2024-05-28T16:52:31Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/C4illin/ConvertX/issues/404", + "title": "WARN[0000] The \"W\" variable is not set. Defaulting to a blank string.", + "repo": "C4illin/ConvertX", + "created": "2025-09-13T15:16:41Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/gitcoinco/web/issues/8521", + "title": "WARNING: The TEST_MNEMONIC variable is not set. Defaulting to a blank string.", + "repo": "gitcoinco/web", + "created": "2021-03-09T23:42:07Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/portainer/portainer/issues/6061", + "title": "Error: Variable is not set. Defaulting to a blank string - when redeploying stack", + "repo": "portainer/portainer", + "created": "2021-11-11T17:36:57Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/sugar-org/compose-go/issues/18", + "title": "WARN[0000] The \"t\" variable is not set. Defaulting to a blank string. ", + "repo": "sugar-org/compose-go", + "created": "2023-04-25T18:30:08Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/IliasHad/edit-mind/issues/125", + "title": "Warnings when execute docker compose up", + "repo": "IliasHad/edit-mind", + "created": "2026-04-27T00:18:26Z", + "state": "open", + "comments": 14, + "verified": true + }, + { + "url": "https://github.com/obytes/superset-image/issues/4", + "title": "WARN[0000] The \"CYPRESS_CONFIG\" variable is not set. Defaulting to a blank string.", + "repo": "obytes/superset-image", + "created": "2024-01-24T22:39:31Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kanbn/kan/issues/530", + "title": "Self Hosting documentation not complete or erroneous", + "repo": "kanbn/kan", + "created": "2026-07-02T09:23:59Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/kalokaman/huly-selfhost/issues/1", + "title": "docker compose up -d", + "repo": "kalokaman/huly-selfhost", + "created": "2026-07-13T10:08:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/rsdancey/BlueSpice-Multiwiki-Install/issues/1", + "title": "Installation failed", + "repo": "rsdancey/BlueSpice-Multiwiki-Install", + "created": "2025-08-04T09:48:40Z", + "state": "open", + "comments": 12, + "verified": true + }, + { + "url": "https://github.com/Graham-Williams/km-tracker/issues/69", + "title": "docker-compose.access.yml still uses bare ${VAR} \u2014 two compose warnings on every deploy", + "repo": "Graham-Williams/km-tracker", + "created": "2026-08-05T15:19:32Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Billionmail/BillionMail/issues/337", + "title": "WARN[0000] The \"BM_API_KEY\" variable is not set. Defaulting to a blank string. bug Something isn't working", + "repo": "Billionmail/BillionMail", + "created": "2026-04-14T09:46:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/FlowiseAI/Flowise/issues/6030", + "title": "Docker Compose setup result in container with \"Error: ENOENT: no such file or directory, mkdir ''\" error", + "repo": "FlowiseAI/Flowise", + "created": "2026-03-21T16:18:57Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/microservices-patterns/ftgo-consumer-web-ui/issues/13", + "title": "WARNING: The FTGO_BACKEND_API_URL variable is not set. Defaulting to a blank string.", + "repo": "microservices-patterns/ftgo-consumer-web-ui", + "created": "2021-05-25T20:48:44Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/isopen-io/meeshy/issues/5624", + "title": "Les hachages d'authentification du .env cessent de fuir dans les journaux", + "repo": "isopen-io/meeshy", + "created": "2026-09-07T20:32:25Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/videre-project/mtgo-docker/issues/1", + "title": "Unable to run on MacOS M1.", + "repo": "videre-project/mtgo-docker", + "created": "2026-04-14T17:30:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/twentyhq/twenty/issues/21035", + "title": "Oneline Installer does not work on a fresh install", + "repo": "twentyhq/twenty", + "created": "2026-05-29T05:04:33Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/issues/12", + "title": "PG image interrupted", + "repo": "mARTin-B78/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama", + "created": "2026-06-03T14:35:26Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/quadratichq/quadratic-selfhost/issues/102", + "title": "not able to setup on local machine on macbook", + "repo": "quadratichq/quadratic-selfhost", + "created": "2026-07-08T06:52:05Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 0.9666666666666667, + "adjusted": 2310 +} \ No newline at end of file diff --git a/bench/data/container-healthcheck-unhealthy.json b/bench/data/container-healthcheck-unhealthy.json new file mode 100644 index 0000000..85a43a6 --- /dev/null +++ b/bench/data/container-healthcheck-unhealthy.json @@ -0,0 +1,280 @@ +{ + "category": "container healthcheck unhealthy", + "rule": "", + "query": "\"is unhealthy\" \"dependency failed to start\" is:issue created:>=2021-01-01", + "total": 1155, + "sample": [ + { + "url": "https://github.com/magenx/Magento-2-docker-configuration/issues/262", + "title": "dependency failed to start: container mariadb is unhealthy", + "repo": "magenx/Magento-2-docker-configuration", + "created": "2026-06-25T18:47:18Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/DadsMmoLab/dads-mmo-lab/issues/77", + "title": "Container vanilla-db is unhealthy", + "repo": "DadsMmoLab/dads-mmo-lab", + "created": "2026-08-15T10:14:51Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/xtreme1-io/xtreme1/issues/311", + "title": "dependency failed to start: container xtreme1-v091-backend-1 is unhealthy", + "repo": "xtreme1-io/xtreme1", + "created": "2025-01-15T13:00:12Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/angolo40/mikrocata2selks/issues/46", + "title": "dependency failed to start: container config-opensearch", + "repo": "angolo40/mikrocata2selks", + "created": "2025-11-27T18:19:38Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/coze-dev/coze-studio/issues/2299", + "title": "dependency failed to start: container coze-elasticsearch is unhealthy", + "repo": "coze-dev/coze-studio", + "created": "2025-09-30T06:23:05Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/overleaf/toolkit/issues/215", + "title": "dependency failed to start: container mongo is unhealthy", + "repo": "overleaf/toolkit", + "created": "2024-02-05T17:25:07Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/coze-dev/coze-studio/issues/2174", + "title": "`dependency failed to start: container coze-etcd is unhealthy`", + "repo": "coze-dev/coze-studio", + "created": "2025-09-15T10:24:49Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/dockersamples/example-voting-app/issues/361", + "title": "Dependency failed to start", + "repo": "dockersamples/example-voting-app", + "created": "2024-07-23T08:17:23Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/truefoundry/cognita/issues/421", + "title": "dependency failed to start: container cognita-postgres is unhealthy", + "repo": "truefoundry/cognita", + "created": "2025-01-16T11:26:23Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/Superflows-AI/superflows/issues/284", + "title": "dependency failed to start: container supabase-analytics is unhealthy", + "repo": "Superflows-AI/superflows", + "created": "2024-11-07T14:26:30Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/StamusNetworks/Clear-NDR-ISO/issues/484", + "title": "dependency failed to start: container selks-db-1 is unhealthy", + "repo": "StamusNetworks/Clear-NDR-ISO", + "created": "2024-12-03T08:46:19Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/frappe/bench/issues/1548", + "title": "dependency failed to start: container frappe-db-1 is unhealthy", + "repo": "frappe/bench", + "created": "2024-03-27T10:32:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/ben-2357/Torrentio-sh-Setup-Guide/issues/3", + "title": "dependency failed to start: container torrentio-selfhostio-mongodb-1 is unhealthy", + "repo": "ben-2357/Torrentio-sh-Setup-Guide", + "created": "2024-02-04T04:28:34Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/muratalioglu/spring-microservices/issues/1", + "title": "Bug: dependency failed to start: container docker-configserver-1 is unhealthy", + "repo": "muratalioglu/spring-microservices", + "created": "2025-09-06T17:37:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/apache/gravitino-playground/issues/112", + "title": "Playground fails to start with error dependency failed to start: container playground-ranger is unhealthy", + "repo": "apache/gravitino-playground", + "created": "2024-12-19T21:18:55Z", + "state": "open", + "comments": 31, + "verified": true + }, + { + "url": "https://github.com/tencentmusic/supersonic/issues/1488", + "title": "[Bug] dependency failed to start: container supersonic_mysql is unhealthy ", + "repo": "tencentmusic/supersonic", + "created": "2024-07-30T09:28:24Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/docker/genai-stack/issues/142", + "title": "dependency failed to start: container genai-stack-api-1 is unhealthy", + "repo": "docker/genai-stack", + "created": "2024-03-13T22:09:51Z", + "state": "open", + "comments": 12, + "verified": true + }, + { + "url": "https://github.com/coollabsio/coolify/issues/11695", + "title": "[Bug]: Manual Installation Fails on Alpine on Podroid", + "repo": "coollabsio/coolify", + "created": "2026-09-09T01:43:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/elastic/beats/issues/53131", + "title": "[Flaky-Test] Integration tests fail to start Elasticsearch container: ` Container libbeat_9_6_0_b05e54ada1-snapshot-elasticsearch-1 Error`", + "repo": "elastic/beats", + "created": "2026-09-09T21:43:38Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/o19s/chorus-opensearch-edition/issues/180", + "title": "Bug: opensearch-mcp-server-py fails healthcheck with 401 Unauthorized during ./quickstart.sh", + "repo": "o19s/chorus-opensearch-edition", + "created": "2026-09-07T19:28:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/apache/gravitino-playground/issues/20", + "title": "Playground fails to start with error dependency failed to start: container playground-gravitino is unhealthy", + "repo": "apache/gravitino-playground", + "created": "2024-01-16T01:15:24Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/geekau/mediastack/issues/64", + "title": "PostgreSQL is unhealthy", + "repo": "geekau/mediastack", + "created": "2026-01-05T14:41:49Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/sourmash-bio/branchwater/issues/133", + "title": "SRA scale search problem/question - healthcheck port impacted local testing deployment", + "repo": "sourmash-bio/branchwater", + "created": "2026-09-17T18:28:13Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/shorin-nikita/lisa/issues/2", + "title": "[BUG] \u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 Container supabase-db Error dependency failed to start: container supabase-db is unhealthy", + "repo": "shorin-nikita/lisa", + "created": "2026-02-02T11:13:27Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Shaunwei/RealChar/issues/515", + "title": "docker start from new\uff1adependency failed to start: container realchar-backend-1 is unhealthy", + "repo": "Shaunwei/RealChar", + "created": "2024-05-20T03:12:12Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/overleaf/toolkit/issues/431", + "title": "Container mongo Started - Error response from daemon on kernel 7.0.13", + "repo": "overleaf/toolkit", + "created": "2026-06-29T15:32:10Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/onecli/onecli/issues/489", + "title": "exec /usr/local/bin/docker-entrypoint.sh: exec format error", + "repo": "onecli/onecli", + "created": "2026-08-11T13:07:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Sorcha-Platform/Sorcha/issues/1652", + "title": "infra: mongo:8 now resolves to 8.3.x, which refuses to start on Linux kernel >= 6.19 (SERVER-121912)", + "repo": "Sorcha-Platform/Sorcha", + "created": "2026-09-15T17:46:47Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/getsentry/self-hosted/issues/4321", + "title": "Sentry Unstable and not being started", + "repo": "getsentry/self-hosted", + "created": "2026-05-10T03:11:48Z", + "state": "open", + "comments": 15, + "verified": true + }, + { + "url": "https://github.com/coze-dev/coze-studio/issues/2627", + "title": "Docker Compose deployment fails: coze-mysql is unhealthy and blocks service startup", + "repo": "coze-dev/coze-studio", + "created": "2026-03-04T14:10:47Z", + "state": "open", + "comments": 1, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 1155 +} \ No newline at end of file diff --git a/bench/data/counts.csv b/bench/data/counts.csv new file mode 100644 index 0000000..4eee4df --- /dev/null +++ b/bench/data/counts.csv @@ -0,0 +1,33 @@ +category,preflight_rule,raw_hits,sampled,verified,precision,adjusted_estimate +memory: OOMKilled,resources.memory,97607,30,11,0.37,35789 +network not found,,18407,30,29,0.97,17793 +image: tag/repo not found,image.exists,7405,30,28,0.93,6911 +volume permission denied,,6073,30,30,1.0,6073 +platform: exec format error,image.platform,5372,30,29,0.97,5193 +address already in use,ports.host,3792,30,30,1.0,3792 +image: pull access denied,image.exists,3730,30,30,1.0,3730 +platform: requested platform mismatch,image.platform,3717,30,30,1.0,3717 +OCI runtime exec failed,,3479,30,30,1.0,3479 +platform: no matching manifest,image.platform,2760,29,29,1.0,2760 +daemon: cannot connect,(root cause),2712,30,30,1.0,2712 +compose: variable not set,compose.env,2390,30,29,0.97,2310 +DNS resolution in container,,2021,30,30,1.0,2021 +build: COPY source not found,build.context,1421,30,30,1.0,1421 +emulation: qemu crash,image.platform,1301,30,30,1.0,1301 +disk: no space left,"resources.disk,host.disk",1241,30,30,1.0,1241 +port already allocated,"ports.conflict,ports.host",1239,30,30,1.0,1239 +container healthcheck unhealthy,,1155,30,30,1.0,1155 +bind: dir onto file,mounts.bind,707,30,30,1.0,707 +bind: source does not exist,mounts.bind,544,30,30,1.0,544 +compose: unsupported property,compose.version,486,30,30,1.0,486 +gpu: no device driver,resources.gpu,457,30,30,1.0,457 +build: BuildKit required,build.buildkit,443,30,29,0.97,428 +build: COPY failed legacy,build.context,417,30,30,1.0,417 +compose: required variable,compose.env,475,30,25,0.83,396 +kernel: vm.max_map_count,kernel.sysctl,369,30,30,1.0,369 +credentials helper missing,registry.credentials,260,30,30,1.0,260 +desktop: mounts denied,mounts.sharing,153,30,30,1.0,153 +app config/env missing at runtime,(learned),118,30,30,1.0,118 +build: outside build context,build.context,65,30,30,1.0,65 +rootless privileged port,ports.host,39,30,26,0.87,34 +memory: mssql minimum,resources.memory,4653,30,0,0.0,0 diff --git a/bench/data/credentials-helper-missing.json b/bench/data/credentials-helper-missing.json new file mode 100644 index 0000000..f8f6d97 --- /dev/null +++ b/bench/data/credentials-helper-missing.json @@ -0,0 +1,280 @@ +{ + "category": "credentials helper missing", + "rule": "registry.credentials", + "query": "\"error getting credentials\" \"docker-credential\" is:issue created:>=2021-01-01", + "total": 260, + "sample": [ + { + "url": "https://github.com/l8knightz/intellect-bi/issues/1", + "title": "Error getting credentials", + "repo": "l8knightz/intellect-bi", + "created": "2025-11-05T04:10:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/crossplane/cli/issues/282", + "title": "`credsStore` in docker config causes unit test failures", + "repo": "crossplane/cli", + "created": "2026-08-18T19:32:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/getumbrel/llama-gpt/issues/9", + "title": "Error getting credentials on m1 mbp", + "repo": "getumbrel/llama-gpt", + "created": "2023-08-16T21:55:24Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7445", + "title": "error getting credentials - err: exec: \"docker-credential-desktop\": executable file not found in $PATH, out: ``", + "repo": "docker/for-mac", + "created": "2024-10-07T07:20:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/boring-design/elastic-fruit-runner/issues/83", + "title": "Docker backend cannot find credential helper under Homebrew service PATH", + "repo": "boring-design/elastic-fruit-runner", + "created": "2026-06-28T19:40:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/rancher/rancher/issues/46868", + "title": "[BUG] WSL integration docker/credential login errors on reference to windows executable ", + "repo": "rancher/rancher", + "created": "2024-08-28T15:33:07Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/cheeselemon/ophiuchi-desktop/issues/18", + "title": "Fix for error getting credentials - err: exec: \"docker-credential-osxkeychain\": executable file not found in $PATH, out: ", + "repo": "cheeselemon/ophiuchi-desktop", + "created": "2024-06-26T08:10:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/subev/libratory/issues/20", + "title": "Crash: Error: Postgres would not start: error getting credentials - err: exec: \"docker-credential", + "repo": "subev/libratory", + "created": "2026-09-11T05:31:36Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/podman-container-tools/container-libs/issues/94", + "title": "Misleading messages when login/logout with docker-credential-pass", + "repo": "podman-container-tools/container-libs", + "created": "2021-06-10T15:37:12Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/anthropics/claude-code/issues/90758", + "title": "Claude ran a credential-helper `get` command and printed a live access token into the transcript", + "repo": "anthropics/claude-code", + "created": "2026-08-30T13:40:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7535", + "title": "err: exec: \"docker-credential-osxkeychain\": executable file not found in $PATH, out", + "repo": "docker/for-mac", + "created": "2025-01-10T07:41:44Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/10443", + "title": "Linux binary directories in Windows `%PATH%` break Docker CLI in other WSL distros (credential helper conflict)", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2026-06-11T00:00:05Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/smol-machines/smolvm/issues/1246", + "title": "support Docker credential helpers (host-side)", + "repo": "smol-machines/smolvm", + "created": "2026-09-13T11:15:33Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/pantsbuild/pants/issues/23411", + "title": "docker_image builds fail with \"docker-credential-osxkeychain not found in $PATH\" after macOS tmp reaper deletes pantsd's materialized immutable_inputs shims", + "repo": "pantsbuild/pants", + "created": "2026-06-09T23:39:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/fabiogra/moseca/issues/15", + "title": "ERROR: failed to solve: python:3.10: failed to resolve source metadata for docker.io/library/python:3.10: error getting credentials", + "repo": "fabiogra/moseca", + "created": "2024-12-06T04:55:55Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/abiosoft/colima/issues/1437", + "title": "Not able to pull image", + "repo": "abiosoft/colima", + "created": "2025-10-26T10:41:07Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/geopython/geopython-workshop/issues/106", + "title": "Can't start workshop on WSL linux on Windows 11", + "repo": "geopython/geopython-workshop", + "created": "2022-08-22T07:22:21Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/3566", + "title": "Setting up credential helper for GCP container registry + nerdctl", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2022-12-07T19:58:58Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/2847", + "title": "Selenium Dynamic Grid failed to start with mounting error", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2022-08-31T14:44:03Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/wupasscat/continent-bot/issues/3", + "title": "Make a Docker container", + "repo": "wupasscat/continent-bot", + "created": "2023-03-01T14:54:16Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/jasonacox/Powerwall-Dashboard/issues/786", + "title": "Vr1 setup problem", + "repo": "jasonacox/Powerwall-Dashboard", + "created": "2026-06-19T09:42:19Z", + "state": "open", + "comments": 48, + "verified": true + }, + { + "url": "https://github.com/podman-container-tools/podman/issues/22462", + "title": "Podman fails to pull images with GCP Docker Credential Helpers", + "repo": "podman-container-tools/podman", + "created": "2024-04-22T22:11:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/GoogleCloudPlatform/docker-credential-gcr/issues/113", + "title": "Handle reauth / invalid_rapt errors more gracefully", + "repo": "GoogleCloudPlatform/docker-credential-gcr", + "created": "2022-05-31T14:35:57Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/coollabsio/coolify/issues/4763", + "title": "[Bug]: pulling private gcr.io (Google Cloud Artifacts Registry) Docker image fails", + "repo": "coollabsio/coolify", + "created": "2025-01-07T08:36:47Z", + "state": "open", + "comments": 13, + "verified": true + }, + { + "url": "https://github.com/earthly/earthly/issues/4320", + "title": "Sanity check not working on OS X + Podman", + "repo": "earthly/earthly", + "created": "2025-02-26T10:27:49Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/runfinch/finch/issues/800", + "title": "Support BYO credential helpers", + "repo": "runfinch/finch", + "created": "2024-02-08T12:19:28Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kinow/auto-mhm-test-domains/issues/5", + "title": "Document singularity & apptainer", + "repo": "kinow/auto-mhm-test-domains", + "created": "2023-04-14T22:14:19Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/cytopia/devilbox/issues/938", + "title": "[Bug]: Pulled the master branch then it won't start anymore", + "repo": "cytopia/devilbox", + "created": "2022-12-16T08:09:28Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/kimhxsong/Inception/issues/2", + "title": "ERROR [internal] load metadata for docker.io/library/...", + "repo": "kimhxsong/Inception", + "created": "2022-06-13T12:42:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/GoogleContainerTools/kaniko/issues/2808", + "title": "On 1.17.0, instance metadata ECR credentials authorization on EC2 doesn't work anymore", + "repo": "GoogleContainerTools/kaniko", + "created": "2023-10-19T17:56:15Z", + "state": "open", + "comments": 2, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 260 +} \ No newline at end of file diff --git a/bench/data/daemon--cannot-connect.json b/bench/data/daemon--cannot-connect.json new file mode 100644 index 0000000..3292d0a --- /dev/null +++ b/bench/data/daemon--cannot-connect.json @@ -0,0 +1,280 @@ +{ + "category": "daemon: cannot connect", + "rule": "(root cause)", + "query": "\"Cannot connect to the Docker daemon\" is:issue created:>=2021-01-01", + "total": 2712, + "sample": [ + { + "url": "https://github.com/PerryRylance/docker-dns-proxy/issues/1", + "title": "Cannot connect to the Docker daemon", + "repo": "PerryRylance/docker-dns-proxy", + "created": "2026-08-27T06:28:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/soumyanilain/handson-l3-SoumyanilAin/issues/2", + "title": "Cannot connect to the Docker daemon at npipe:////./pipe/docker_engine", + "repo": "soumyanilain/handson-l3-SoumyanilAin", + "created": "2026-09-01T21:13:42Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/linuxserver/docker-kasm/issues/110", + "title": "[BUG] Kasm does not start after an unclean shutdown when the stale containerd pid belongs to another process", + "repo": "linuxserver/docker-kasm", + "created": "2026-09-23T13:12:29Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/sbx-releases/issues/157", + "title": "sandboxd v0.30.0 on Windows: in-process moby backend cannot self-connect to its own `docker.sock` (\"Cannot connect to the Docker daemon at unix://...docker.sock\")", + "repo": "docker/sbx-releases", + "created": "2026-05-19T15:56:15Z", + "state": "open", + "comments": 15, + "verified": true + }, + { + "url": "https://github.com/addnab/docker-run-action/issues/50", + "title": "Cannot connect to the Docker daemon", + "repo": "addnab/docker-run-action", + "created": "2024-01-26T09:49:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/nbx0/NGS_training/issues/2", + "title": "cannot connect to the docker daemon", + "repo": "nbx0/NGS_training", + "created": "2022-11-23T14:11:00Z", + "state": "open", + "comments": 15, + "verified": true + }, + { + "url": "https://github.com/sickcodes/Docker-OSX/issues/660", + "title": "Cannot connect to the Docker daemon at", + "repo": "sickcodes/Docker-OSX", + "created": "2023-05-17T19:07:17Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/erkylima/react-microfront/issues/1", + "title": "`version` is obsolete and \"Cannot connect to the Docker daemon\"", + "repo": "erkylima/react-microfront", + "created": "2024-05-31T06:35:08Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/microcks/microcks-cli/issues/537", + "title": "macOS: honor the active Docker context when /var/run/docker.sock is unavailable", + "repo": "microcks/microcks-cli", + "created": "2026-09-22T04:44:06Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/bountybench/bountytasks/issues/285", + "title": "`./run_ci_local` Cannot connect to the Docker daemon", + "repo": "bountybench/bountytasks", + "created": "2025-02-22T07:18:15Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/deviceplane/agent/issues/3", + "title": "Cannot connect to the Docker daemon, every 3 seconds", + "repo": "deviceplane/agent", + "created": "2022-01-11T08:43:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/AleksandrJelohhin/flaketriage/issues/2", + "title": "Infra signatures: Docker daemon unreachable and HTTP 429 rate limiting", + "repo": "AleksandrJelohhin/flaketriage", + "created": "2026-09-11T20:06:36Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Osmantic/ODS/issues/3823", + "title": "ods status / restart / start hide the real Docker error when the daemon is unreachable (\"no configuration file provided\", \"rootless ownership is not ready\")", + "repo": "Osmantic/ODS", + "created": "2026-09-07T19:06:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/belal-i/docksible/issues/16", + "title": "\"Cannot connect to the Docker daemon\" on some local environments", + "repo": "belal-i/docksible", + "created": "2025-02-18T21:54:07Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/containrrr/watchtower/issues/2023", + "title": "Cannot connect to the Docker daemon after Docker daemon update", + "repo": "containrrr/watchtower", + "created": "2024-09-16T09:35:35Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/devcontainers/features/issues/977", + "title": "Codespaces: \"Cannot connect to the Docker daemon\" occurs randomly (postCreateCommand)", + "repo": "devcontainers/features", + "created": "2024-05-18T08:50:20Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/drone-plugins/drone-docker/issues/414", + "title": "Cannot connect to the Docker daemon at unix:///var/run/docker.sock.", + "repo": "drone-plugins/drone-docker", + "created": "2023-10-25T11:00:02Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/githedgehog/fabricator/issues/1642", + "title": "VLAB ERROR: Cannot connect to the Docker daemon at unix:///run/docker/docker.sock", + "repo": "githedgehog/fabricator", + "created": "2026-04-08T12:17:35Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/cherishman2005/nginx-modules/issues/57", + "title": "[docker]Cannot connect to the Docker daemon at unix:///var/run/docker.sock.", + "repo": "cherishman2005/nginx-modules", + "created": "2023-03-30T05:05:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/desktop-linux/issues/20", + "title": "Cannot connect to the Docker daemon after installing docker desktop on Linux", + "repo": "docker/desktop-linux", + "created": "2022-05-13T21:37:46Z", + "state": "open", + "comments": 32, + "verified": true + }, + { + "url": "https://github.com/lil5/lazypodman/issues/2", + "title": "Looking for docker?", + "repo": "lil5/lazypodman", + "created": "2026-09-12T17:58:32Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-win/issues/14759", + "title": "Cannot connect to the Docker daemon at unix:///var/run/docker.sock.", + "repo": "docker/for-win", + "created": "2025-05-05T16:07:49Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/actions/actions-runner-controller/issues/3794", + "title": "Intermittently getting \"Cannot connect to the Docker daemon at unix:///var/run/docker.sock\"", + "repo": "actions/actions-runner-controller", + "created": "2024-11-05T11:21:43Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/nektos/act/issues/5836", + "title": "`Cannot connect to the Docker daemon` when calling `docker run` inside a local action", + "repo": "nektos/act", + "created": "2025-06-24T17:46:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/EinSpi/scripts/issues/24", + "title": "docker\u91cd\u542f\u5931\u8d25\u62a5\u9519", + "repo": "EinSpi/scripts", + "created": "2026-08-20T06:57:49Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/solutionforest/ephemeral-action-runner/issues/51", + "title": "Windows Docker Sandboxes startup blocked by sbx internal image API failure", + "repo": "solutionforest/ephemeral-action-runner", + "created": "2026-09-23T06:02:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/pagopa/github-self-hosted-runner-azure/issues/16", + "title": "Pipleine fail: Cannot connect to the Docker daemon at unix:///var/run/docker.sock", + "repo": "pagopa/github-self-hosted-runner-azure", + "created": "2023-11-28T22:07:56Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/cnrancher/autok3s/issues/524", + "title": "[BUG] docker-compose - windows - Cannot connect to the Docker daemon at unix:///var/run/docker.sock", + "repo": "cnrancher/autok3s", + "created": "2022-11-14T11:31:09Z", + "state": "open", + "comments": 17, + "verified": true + }, + { + "url": "https://github.com/docker/for-win/issues/14910", + "title": "Virtualization support not detected", + "repo": "docker/for-win", + "created": "2025-08-18T07:15:41Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/keploy/keploy/issues/3109", + "title": "[bug]: If the current docker context is not default then use the correct context", + "repo": "keploy/keploy", + "created": "2025-10-09T13:36:36Z", + "state": "open", + "comments": 4, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 2712 +} \ No newline at end of file diff --git a/bench/data/desktop--mounts-denied.json b/bench/data/desktop--mounts-denied.json new file mode 100644 index 0000000..4f992b5 --- /dev/null +++ b/bench/data/desktop--mounts-denied.json @@ -0,0 +1,280 @@ +{ + "category": "desktop: mounts denied", + "rule": "mounts.sharing", + "query": "\"Mounts denied\" \"is not shared from the host\" is:issue created:>=2021-01-01", + "total": 153, + "sample": [ + { + "url": "https://github.com/docker/for-mac/issues/6759", + "title": "Error response from daemon: Mounts denied: The path **** is not shared from the host and is not known to Docker.", + "repo": "docker/for-mac", + "created": "2023-03-09T20:28:17Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/top-bettercode/docker-wechat/issues/55", + "title": "docker: Error response from daemon: Mounts denied", + "repo": "top-bettercode/docker-wechat", + "created": "2023-02-14T21:49:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ahoone/ahoonepi-proxypi/issues/14", + "title": "Mount volume denied on fresh installation", + "repo": "ahoone/ahoonepi-proxypi", + "created": "2026-07-07T10:27:35Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7589", + "title": "v4.38.0 Pro - Mounts denied - /mutagen-file-shares/fshr_553e66d098fc19abc8fb8c715ad7666d3898cac7000/root is not shared from the host and is not known to Docker", + "repo": "docker/for-mac", + "created": "2025-02-12T05:04:39Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/wizeline/docker-wordpress-minimal/issues/2", + "title": "Error on Mac OSX", + "repo": "wizeline/docker-wordpress-minimal", + "created": "2021-08-03T18:22:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/passivedns/pdns-docker/issues/1", + "title": "Prod environment not working properly on macos", + "repo": "passivedns/pdns-docker", + "created": "2024-07-03T14:16:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/spinframework/otel-plugin/issues/37", + "title": "issue running `spin otel setup`", + "repo": "spinframework/otel-plugin", + "created": "2026-04-02T22:25:04Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/AudreyBeard/compressure/issues/47", + "title": "Make Build Failing", + "repo": "AudreyBeard/compressure", + "created": "2023-09-01T04:02:34Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/KatharaFramework/Kathara/issues/388", + "title": "macOS Docker Desktop shared mount path issue when running Kathara inside a container", + "repo": "KatharaFramework/Kathara", + "created": "2026-05-11T22:17:17Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Azure-Samples/azure-postgres-pgvector-python/issues/35", + "title": "Workspace path error when using Docker Desktop for Mac with VSCode devcontainers extension", + "repo": "Azure-Samples/azure-postgres-pgvector-python", + "created": "2024-11-21T19:07:39Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RADIUSdesk/rdcore/issues/39", + "title": "Doesnt Work", + "repo": "RADIUSdesk/rdcore", + "created": "2023-06-18T12:37:03Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/sickcodes/Docker-OSX/issues/673", + "title": "Error running Docker-OSX on Fedora", + "repo": "sickcodes/Docker-OSX", + "created": "2023-06-28T08:14:53Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/tsukasaI/serverless_with_go/issues/1", + "title": "provided.al2 not running", + "repo": "tsukasaI/serverless_with_go", + "created": "2023-11-09T05:17:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/microsoft/vscode-remote-release/issues/5388", + "title": "Clone repository in container volume with Docker Compose fails due to permissions for non-existant folder", + "repo": "microsoft/vscode-remote-release", + "created": "2021-07-27T18:57:15Z", + "state": "open", + "comments": 16, + "verified": true + }, + { + "url": "https://github.com/athombv/homey-apps-sdk-issues/issues/303", + "title": "SDK Docs - Please add Docker fileshare guide ", + "repo": "athombv/homey-apps-sdk-issues", + "created": "2023-07-10T07:57:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/eliasgranderubio/dagda/issues/103", + "title": "docker-compose.yaml unfriendly to macos version of docker?", + "repo": "eliasgranderubio/dagda", + "created": "2022-04-07T18:33:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ansible/ansible-navigator/issues/1286", + "title": "Unable to extract collection json from stdout, when listing collections through navigator", + "repo": "ansible/ansible-navigator", + "created": "2022-09-08T17:00:59Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/CIDARLAB/homework1-template/issues/1", + "title": "RuntimeError: Unable to find logic.csv using poetry", + "repo": "CIDARLAB/homework1-template", + "created": "2021-03-29T04:11:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ozee/vm-workspace/issues/1", + "title": "Failed tasks when running the application", + "repo": "ozee/vm-workspace", + "created": "2023-06-14T19:59:51Z", + "state": "open", + "comments": 27, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7105", + "title": "Trying to mount directories in Docker", + "repo": "docker/for-mac", + "created": "2023-12-08T19:47:43Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/php-actions/composer/issues/97", + "title": "Exit code 125 and .gitconfig error ", + "repo": "php-actions/composer", + "created": "2023-02-12T14:49:50Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/RADIUSdesk/rdcore/issues/41", + "title": "Unable to create Docker image ", + "repo": "RADIUSdesk/rdcore", + "created": "2023-07-24T20:43:01Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/tabledog/datasette-stripe/issues/1", + "title": "Suggestion for \"Docker Run\" section of README", + "repo": "tabledog/datasette-stripe", + "created": "2022-07-21T02:15:28Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/lando/lando/issues/3472", + "title": "Docker warnings \u201cYou have opted to share your home directory with a container\u201d", + "repo": "lando/lando", + "created": "2022-11-22T22:44:36Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/testcontainers/testcontainers-python/issues/621", + "title": "todo - run tests against docker desktop on linux", + "repo": "testcontainers/testcontainers-python", + "created": "2024-06-28T03:03:34Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/Part-DB/Part-DB-server/issues/437", + "title": "Help needed installing Part-DB on MacOS in Docker", + "repo": "Part-DB/Part-DB-server", + "created": "2023-11-24T08:36:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/DataTalksClub/faq/issues/291", + "title": "[FAQ] Kestra does not run because of Docker error \"mounts denied: The path /tmp/... is not shared from the host\"", + "repo": "DataTalksClub/faq", + "created": "2026-07-01T10:14:46Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7478", + "title": "Docker run doesn't exit on macOS in case of wrong mount", + "repo": "docker/for-mac", + "created": "2024-11-07T09:20:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7519", + "title": "Not able to add paths to latest Docker Desktop File Sharing on MacOS Monterey", + "repo": "docker/for-mac", + "created": "2025-01-05T12:36:26Z", + "state": "open", + "comments": 22, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7514", + "title": "Synchronized file shares can not mount a single file", + "repo": "docker/for-mac", + "created": "2024-12-28T05:18:27Z", + "state": "open", + "comments": 1, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 153 +} \ No newline at end of file diff --git a/bench/data/disk--no-space-left.json b/bench/data/disk--no-space-left.json new file mode 100644 index 0000000..83ed63a --- /dev/null +++ b/bench/data/disk--no-space-left.json @@ -0,0 +1,280 @@ +{ + "category": "disk: no space left", + "rule": "resources.disk,host.disk", + "query": "\"no space left on device\" docker pull is:issue created:>=2021-01-01", + "total": 1241, + "sample": [ + { + "url": "https://github.com/TonyTromp/tidal-connect-docker/issues/13", + "title": "docker pull --> no space left on device", + "repo": "TonyTromp/tidal-connect-docker", + "created": "2022-03-08T09:11:42Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/SCUTlihaoyu/open-chat-video-editor/issues/14", + "title": "no space left on device", + "repo": "SCUTlihaoyu/open-chat-video-editor", + "created": "2023-05-11T14:41:48Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Dispatcharr/Dispatcharr/issues/692", + "title": "[Bug-Fix]: No space left on device (Postgres-issue)", + "repo": "Dispatcharr/Dispatcharr", + "created": "2025-11-27T22:01:05Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/awslabs/genomics-secondary-analysis-using-aws-step-functions-and-aws-batch/issues/9", + "title": "No Space left on device", + "repo": "awslabs/genomics-secondary-analysis-using-aws-step-functions-and-aws-batch", + "created": "2021-11-01T19:10:47Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/openpredictionmarkets/socialpredict/issues/374", + "title": "No Space Left on Device Error", + "repo": "openpredictionmarkets/socialpredict", + "created": "2024-10-26T16:55:02Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/david-coincodex/pm/issues/110", + "title": "Production deploy never prunes old images \u2014 inode exhaustion took Postgres down for ~9h", + "repo": "david-coincodex/pm", + "created": "2026-09-21T09:11:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/vmware-archive/vctl-docs/issues/48", + "title": "no space left on device when `vctl pull`", + "repo": "vmware-archive/vctl-docs", + "created": "2021-06-23T17:11:58Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/y-taka-23/wasm-pandoc/issues/3", + "title": "no space left on device", + "repo": "y-taka-23/wasm-pandoc", + "created": "2022-03-13T08:02:54Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tlc-pack/tlcpack/issues/186", + "title": "[CI] [Conda] [Cuda] No space left on device", + "repo": "tlc-pack/tlcpack", + "created": "2023-08-03T14:57:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/vectordotdev/vector/issues/22288", + "title": "Vector keeps reading from a log file that has been deleted when no space left on device", + "repo": "vectordotdev/vector", + "created": "2025-01-23T03:28:10Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/asyncapi/cli/issues/1966", + "title": "linux/arm64 Docker build fails with \"No space left on device\" during chown", + "repo": "asyncapi/cli", + "created": "2026-01-08T14:11:38Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/autowarefoundation/autoware/issues/5840", + "title": "No space left on device for docker-build workflow", + "repo": "autowarefoundation/autoware", + "created": "2025-03-05T04:01:23Z", + "state": "open", + "comments": 14, + "verified": true + }, + { + "url": "https://github.com/ROCm/ROCm-docker/issues/92", + "title": "Pulling docker image uses too many inodes (no space left on device)", + "repo": "ROCm/ROCm-docker", + "created": "2022-06-13T22:03:06Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tektoncd/catalog/issues/1305", + "title": "Tekton Buildah Task failed No Space left On device", + "repo": "tektoncd/catalog", + "created": "2024-10-02T09:28:49Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/driftless-star/driftless-star/issues/92", + "title": "fix: Apptainer GPU builds fail with \"no space left on device\" during SIF creation", + "repo": "driftless-star/driftless-star", + "created": "2026-06-18T20:48:23Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/tinaudio/synth-setter/issues/2351", + "title": "fix(ci-automation): prevent VST runner disk exhaustion during image pulls", + "repo": "tinaudio/synth-setter", + "created": "2026-07-23T18:09:06Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nf-core/viralrecon/issues/497", + "title": "MAKE_VARIANTS_LONG_TABLE fails in CI build due to no space left on device", + "repo": "nf-core/viralrecon", + "created": "2025-04-30T17:24:11Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/jonohill/docker-openvpn-proxy/issues/14", + "title": "No space left on device: many files created.", + "repo": "jonohill/docker-openvpn-proxy", + "created": "2024-01-18T09:32:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/leanprover-community/docgen-action/issues/16", + "title": "Error: `docker: failed to register layer: write [...] no space left on device`", + "repo": "leanprover-community/docgen-action", + "created": "2025-12-29T20:36:43Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/isopen-io/meeshy/issues/6556", + "title": "Le disque de l'h\u00f4te de staging est plein : \u00ab Deploy to staging \u00bb \u00e9choue au `docker pull`, et staging sert une version p\u00e9rim\u00e9e en silence", + "repo": "isopen-io/meeshy", + "created": "2026-09-14T17:14:36Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/jonaswinkler/paperless-ng/issues/1171", + "title": "[BUG] Error during installation: \"[Errno 28] No space left on device\"", + "repo": "jonaswinkler/paperless-ng", + "created": "2021-07-15T13:23:47Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/airbytehq/airbyte/issues/71134", + "title": "[abctl] fills up /tmp directory causing \"no space left on device\" errors", + "repo": "airbytehq/airbyte", + "created": "2026-01-06T12:41:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ollama/ollama/issues/1731", + "title": "`pulling manifest Error: EOF` when pulling after disk is full", + "repo": "ollama/ollama", + "created": "2023-12-28T00:32:29Z", + "state": "open", + "comments": 12, + "verified": true + }, + { + "url": "https://github.com/appotry/docker-hexo/issues/42", + "title": "no space left on device \u4f46\u6211U\u76d8\u670920G\u5bb9\u91cf", + "repo": "appotry/docker-hexo", + "created": "2026-09-16T06:57:39Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/sheynkman-lab/LRP2/issues/144", + "title": "Investigate optimizing Apptainer and Singularity caching setup", + "repo": "sheynkman-lab/LRP2", + "created": "2026-07-30T18:42:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/podman-container-tools/buildah/issues/4423", + "title": "No space left on device while building 3.26Gb image with 11Gb space available", + "repo": "podman-container-tools/buildah", + "created": "2022-11-18T09:15:05Z", + "state": "open", + "comments": 33, + "verified": true + }, + { + "url": "https://github.com/soflyy/katalystwp/issues/84", + "title": "Docker disk exhaustion: orphan images, layer generations, and health reporting the wrong volume", + "repo": "soflyy/katalystwp", + "created": "2026-09-20T04:09:04Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/MinaProtocol/mina/issues/19374", + "title": "Verify mina-toolchain cache job never finishes: it validates every tag ever cached, not the build's", + "repo": "MinaProtocol/mina", + "created": "2026-09-06T03:05:13Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/tektoncd/pipeline/issues/9052", + "title": "Alpha e2e tests fail often", + "repo": "tektoncd/pipeline", + "created": "2025-10-01T08:53:10Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/QuantumNous/new-api/issues/5878", + "title": "ARMv7 / \u73a9\u5ba2\u4e91 \u73af\u5883\u4e0b\u9ed8\u8ba4 Docker \u6784\u5efa\u94fe\u8def\u517c\u5bb9\u6027\u8f83\u5dee", + "repo": "QuantumNous/new-api", + "created": "2026-07-03T10:31:16Z", + "state": "open", + "comments": 3, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 1241 +} \ No newline at end of file diff --git a/bench/data/dns-resolution-in-container.json b/bench/data/dns-resolution-in-container.json new file mode 100644 index 0000000..a607c6e --- /dev/null +++ b/bench/data/dns-resolution-in-container.json @@ -0,0 +1,280 @@ +{ + "category": "DNS resolution in container", + "rule": "", + "query": "\"Temporary failure in name resolution\" docker is:issue created:>=2021-01-01", + "total": 2021, + "sample": [ + { + "url": "https://github.com/langgenius/dify/issues/40601", + "title": "http Request [Errno -3] Temporary failure in name resolution", + "repo": "langgenius/dify", + "created": "2026-08-12T08:45:54Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/synesthesiam/homeassistant-satellite/issues/79", + "title": "Temporary failure in name resolution", + "repo": "synesthesiam/homeassistant-satellite", + "created": "2023-12-02T18:41:47Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/johanmeijer/grott/issues/296", + "title": "Temporary failure in name resolution", + "repo": "johanmeijer/grott", + "created": "2023-02-19T17:31:42Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/octavius64/Gazelle/issues/4", + "title": "Temporary failure in name resolution", + "repo": "octavius64/Gazelle", + "created": "2023-03-18T08:17:44Z", + "state": "open", + "comments": 12, + "verified": true + }, + { + "url": "https://github.com/raskitoma/pls_admin/issues/9", + "title": "plsworker error: Temporary failure in name resolution...", + "repo": "raskitoma/pls_admin", + "created": "2024-07-29T11:04:41Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/samply/focus/issues/386", + "title": "Startup since 0.24.1", + "repo": "samply/focus", + "created": "2026-09-21T12:25:50Z", + "state": "open", + "comments": 20, + "verified": true + }, + { + "url": "https://github.com/StackStorm/st2-docker/issues/216", + "title": "Problem installing or searching pack, temporary failure in name resolution", + "repo": "StackStorm/st2-docker", + "created": "2021-02-26T00:10:13Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/docker/for-win/issues/13808", + "title": "Error \"Temporary failure in name resolution\" when resolving hosts", + "repo": "docker/for-win", + "created": "2023-11-24T11:27:14Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/Azure/azure-cli/issues/26213", + "title": "az login problem: [Errno -3] Temporary failure in name resolution", + "repo": "Azure/azure-cli", + "created": "2023-04-20T12:16:01Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/schubergphilis/towerlib/issues/108", + "title": "CI test failled (Temporary failure in name resolution)", + "repo": "schubergphilis/towerlib", + "created": "2022-10-04T09:35:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/aau-zid/BigBlueButton-liveStreaming/issues/170", + "title": "Docker Compose Error: Temporary Failure in Name Resolution for BigBlueButton Instance", + "repo": "aau-zid/BigBlueButton-liveStreaming", + "created": "2023-09-06T13:39:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/scylladb/scylladb/issues/11967", + "title": "scylla docker scylla_io_setup fails - hostname: Temporary failure in name resolution", + "repo": "scylladb/scylladb", + "created": "2022-11-14T11:15:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/MODSetter/SurfSense/issues/1716", + "title": "Unable to add OpenAI compatible Local AI providers such as LM Studio or Lemonade", + "repo": "MODSetter/SurfSense", + "created": "2026-08-25T15:11:19Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/OpenSPG/KAG/issues/535", + "title": "[Bug] [Module Name] Bug title java.net.UnknownHostException: mysql: Temporary failure in name resolution", + "repo": "OpenSPG/KAG", + "created": "2025-05-13T08:46:22Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/UnstoppableMango/the-cluster/issues/4458", + "title": "Node resolvers have no upstream fallback, making a two-replica pihole outage unrecoverable", + "repo": "UnstoppableMango/the-cluster", + "created": "2026-09-20T19:45:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/MarkusMcNugen/docker-qBittorrentvpn/issues/121", + "title": "docker freezes ", + "repo": "MarkusMcNugen/docker-qBittorrentvpn", + "created": "2022-02-26T18:40:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/midas-research/audino/issues/112", + "title": "Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')", + "repo": "midas-research/audino", + "created": "2023-05-25T05:56:07Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/oracle/graalvm-reachability-metadata/issues/9630", + "title": "Bug: transient network outage during the host gate kills the do-work loop permanently", + "repo": "oracle/graalvm-reachability-metadata", + "created": "2026-08-29T19:11:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/podman-container-tools/podman/issues/24790", + "title": "`podman login` works fine and authenticates but `podman pull` fails with `dial tcp: lookup: Temporary failure in name resolution`", + "repo": "podman-container-tools/podman", + "created": "2024-12-06T19:42:35Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/bluerobotics/BlueOS/issues/1695", + "title": "bug: No Extensions available.", + "repo": "bluerobotics/BlueOS", + "created": "2023-05-23T08:02:01Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/infiniflow/ragflow/issues/19117", + "title": "[Question]: Getting error when configuring postgres as metadata db", + "repo": "infiniflow/ragflow", + "created": "2026-09-01T13:03:03Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Bess-Gosling/zaproxy/issues/381", + "title": "ZAP error messages when DNS is not working in docker scripts could be more forth coming", + "repo": "Bess-Gosling/zaproxy", + "created": "2026-09-10T08:54:47Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/lnsolspvtltd/propops/issues/76", + "title": "Post-deploy propagation steps for PropOps", + "repo": "lnsolspvtltd/propops", + "created": "2026-08-04T18:50:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/podman-container-tools/podman/issues/16342", + "title": "No retry on temporary failure in network name resolution", + "repo": "podman-container-tools/podman", + "created": "2022-10-29T09:46:16Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/cooneycw/mcp-second-opinion/issues/5", + "title": "Attach aws-secrets-agent sidecar to external mcp-net durably (fixes recurring gemma-bot outages)", + "repo": "cooneycw/mcp-second-opinion", + "created": "2026-08-02T16:10:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DecaturMakers/equipment-status-board/issues/82", + "title": "App fails to boot when Slack is unreachable: init_slack() does a blocking auth.test during create_app()", + "repo": "DecaturMakers/equipment-status-board", + "created": "2026-08-29T13:26:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/demonsxxxxxx/ai-platform/issues/409", + "title": "B2: Docker egress evidence lacks policy-bound deny proof", + "repo": "demonsxxxxxx/ai-platform", + "created": "2026-07-12T12:38:11Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/farabisina25/echofinder-test/issues/142", + "title": "Docker Networking", + "repo": "farabisina25/echofinder-test", + "created": "2025-12-20T16:44:54Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/farabisina25/echofinder-test/issues/131", + "title": "Docker Networking Failure", + "repo": "farabisina25/echofinder-test", + "created": "2025-12-20T15:54:57Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/ramon-victor/freegpt-webui/issues/45", + "title": "synology docker enable automatic proxy", + "repo": "ramon-victor/freegpt-webui", + "created": "2023-07-05T14:01:27Z", + "state": "open", + "comments": 2, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 2021 +} \ No newline at end of file diff --git a/bench/data/emulation--qemu-crash.json b/bench/data/emulation--qemu-crash.json new file mode 100644 index 0000000..38aadae --- /dev/null +++ b/bench/data/emulation--qemu-crash.json @@ -0,0 +1,280 @@ +{ + "category": "emulation: qemu crash", + "rule": "image.platform", + "query": "\"qemu: uncaught target signal\" is:issue created:>=2021-01-01", + "total": 1301, + "sample": [ + { + "url": "https://github.com/llvm/llvm-project/issues/223314", + "title": "[libcxx][test] qemu: uncaught target signal 4 (Illegal instruction) - core dumped", + "repo": "llvm/llvm-project", + "created": "2026-09-14T06:58:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/2moe/tmoe/issues/273", + "title": "qemu: uncaught target signal 11", + "repo": "2moe/tmoe", + "created": "2023-02-25T15:17:11Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/askdba/myvector/issues/138", + "title": "docker-publish.yml: emulated arm64 apt install crashed with QEMU segfault (flaky)", + "repo": "askdba/myvector", + "created": "2026-09-22T06:32:22Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/montimage-projects/mmt-dpi/issues/421", + "title": "ci: emulated ldconfig intermittently segfaults on ubuntu-22.04 arm64 package job", + "repo": "montimage-projects/mmt-dpi", + "created": "2026-09-23T05:15:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RPi-Distro/pi-gen/issues/656", + "title": "Error: qemu: uncaught target signal 11, Segmentation fault", + "repo": "RPi-Distro/pi-gen", + "created": "2022-12-07T15:25:47Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/redis/redis/issues/10794", + "title": "[BUG] qemu: uncaught target signal 11 (Segmentation fault)", + "repo": "redis/redis", + "created": "2022-05-29T13:11:48Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/evilmartians/fullstaq-ruby-docker/issues/17", + "title": "qemu: uncaught target signal 6 (Aborted) - core dumped", + "repo": "evilmartians/fullstaq-ruby-docker", + "created": "2022-08-23T09:54:55Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/dullage/eyelign/issues/21", + "title": "qemu: uncaught target signal 4 (Illegal instruction) - core dumped", + "repo": "dullage/eyelign", + "created": "2023-08-19T00:52:25Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/travisgoodspeed/md380tools/issues/953", + "title": "qemu: uncaught target signal 11 (Segmentation fault) - core dumped", + "repo": "travisgoodspeed/md380tools", + "created": "2024-07-14T21:10:03Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/gtrll/gpmp2/issues/52", + "title": "qemu: uncaught target signal 11 (Segmentation fault) - core dumped", + "repo": "gtrll/gpmp2", + "created": "2021-08-29T03:05:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/openswoole/ext-openswoole/issues/332", + "title": "qemu: uncaught target signal 11 (Segmentation fault) - core dumped", + "repo": "openswoole/ext-openswoole", + "created": "2023-10-01T01:38:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/lmnr-ai/lmnr/issues/913", + "title": "Self Hosted version does not work", + "repo": "lmnr-ai/lmnr", + "created": "2025-09-28T21:33:57Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/exadel-inc/CompreFace/issues/673", + "title": "\"qemu: uncaught target signal 6 (Aborted) - core dumped\" on m1 Mac", + "repo": "exadel-inc/CompreFace", + "created": "2021-12-07T23:19:44Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/isopen-io/meeshy/issues/6439", + "title": "La construction multi-architecture de Release rougit main une fois sur deux sans d\u00e9faut de code \u2014 \u00e9mulation arm64 et extraction bun instables", + "repo": "isopen-io/meeshy", + "created": "2026-09-14T01:45:21Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/big-data-europe/docker-hive/issues/43", + "title": "Getting qemu: uncaught target signal 11 (Segmentation fault) - core dumped Segmentation fault", + "repo": "big-data-europe/docker-hive", + "created": "2021-05-07T04:23:19Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/gingrspacecadet/coda/issues/4", + "title": "Compiled programs segfault when an if statement prints and then doesn't return", + "repo": "gingrspacecadet/coda", + "created": "2026-07-13T13:36:35Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/tjakobsson/brain/issues/27", + "title": "Candidate image build hangs after QEMU SIGILL in arm64 npm ci (v1.6.0 release blocked)", + "repo": "tjakobsson/brain", + "created": "2026-09-03T05:30:49Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/softcite/software-mentions/issues/29", + "title": "server crashes with \"qemu: uncaught target signal 6\" on Mac M1 silicon", + "repo": "softcite/software-mentions", + "created": "2023-07-31T18:38:54Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/tonistiigi/binfmt/issues/298", + "title": "Segfault on libc-bin", + "repo": "tonistiigi/binfmt", + "created": "2026-02-18T10:09:11Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/hatchet-dev/hatchet-python-quickstart/issues/1", + "title": "Migration container failing with `qemu: uncaught target signal 11 (Segmentation fault) - core dumped`", + "repo": "hatchet-dev/hatchet-python-quickstart", + "created": "2024-02-22T19:49:09Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/storaged-project/udisks/issues/1525", + "title": "spawned_job/abnormal_termination test fails when cross-testing arm64 under qemu-user", + "repo": "storaged-project/udisks", + "created": "2026-06-17T14:34:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/doemefu/homelab-device-service/issues/73", + "title": "ci: add timeout-minutes guards to the multi-arch build job (same hang class as furchert-ch#41)", + "repo": "doemefu/homelab-device-service", + "created": "2026-09-04T03:21:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7884", + "title": "Embedded BuildKit builder segfaults on linux/amd64 emulated rustc (QEMU signal 11), independent of Rosetta / Docker VMM", + "repo": "docker/for-mac", + "created": "2026-09-15T16:06:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/doemefu/homelab-auth-service/issues/87", + "title": "ci: add timeout-minutes guards to the multi-arch build job (same hang class as furchert-ch#41)", + "repo": "doemefu/homelab-auth-service", + "created": "2026-09-04T03:20:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ziglang/zig/issues/17430", + "title": "-fno-PIC targeting glibc should be legal", + "repo": "ziglang/zig", + "created": "2023-10-07T14:38:17Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/chivay/shellcodez/issues/1", + "title": "Larger payloads cause segmentation faults", + "repo": "chivay/shellcodez", + "created": "2026-08-14T05:37:44Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/bun913/go_lambda_functions/issues/3", + "title": "Apple Slicon\u3067 sam-cli\u306e\u30ed\u30fc\u30ab\u30eb\u5b9f\u884c\u304c\u3067\u304d\u306a\u3044\u554f\u984c\u3092Issue\u306b\u6295\u7a3f\u3059\u308b", + "repo": "bun913/go_lambda_functions", + "created": "2022-05-02T20:37:06Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/k8gb-io/k8gb/issues/2527", + "title": "[BUG] Local playground serves no DNS on arm64: edge BIND image is amd64-only and segfaults under emulation", + "repo": "k8gb-io/k8gb", + "created": "2026-09-08T13:35:08Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/xiangsx/gpt4free-ts/issues/46", + "title": "mac m1\u82af\u7247\u8fd0\u884camd64\u7248\u672c\u955c\u50cf\u62a5\u9519\uff0c\u65e0\u6cd5\u542f\u52a8\u6d4f\u89c8\u5668", + "repo": "xiangsx/gpt4free-ts", + "created": "2023-06-08T01:25:15Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/apache/iceberg/issues/15943", + "title": "Build 'iceberg-rest-fixture' Docker Image random failure", + "repo": "apache/iceberg", + "created": "2026-04-11T16:52:18Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 1301 +} \ No newline at end of file diff --git a/bench/data/gpu--no-device-driver.json b/bench/data/gpu--no-device-driver.json new file mode 100644 index 0000000..584ef4a --- /dev/null +++ b/bench/data/gpu--no-device-driver.json @@ -0,0 +1,280 @@ +{ + "category": "gpu: no device driver", + "rule": "resources.gpu", + "query": "\"could not select device driver\" \"capabilities: [[gpu]]\" is:issue created:>=2021-01-01", + "total": 457, + "sample": [ + { + "url": "https://github.com/dev-it-with-me/RagUltimateAdvisor/issues/3", + "title": "Could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "dev-it-with-me/RagUltimateAdvisor", + "created": "2026-04-01T07:56:55Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/xserrat/docker-facebook-demucs/issues/47", + "title": "[Bug]: could not select device driver with capabilities [[gpu]]", + "repo": "xserrat/docker-facebook-demucs", + "created": "2026-02-28T15:07:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kinu-garage/hut_10sqft/issues/818", + "title": "p16s: rocker fails: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "kinu-garage/hut_10sqft", + "created": "2023-04-04T13:49:18Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/LinkSoul-AI/Chinese-Llama-2-7b/issues/32", + "title": "could not select device driver \"\" with capabilities: [[gpu]]", + "repo": "LinkSoul-AI/Chinese-Llama-2-7b", + "created": "2023-07-27T06:33:52Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/basujindal/stable-diffusion/issues/217", + "title": "could not select device driver \"nvidia\" with capabilities [[gpu]]", + "repo": "basujindal/stable-diffusion", + "created": "2022-11-03T05:33:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/jianchang512/ChatTTS-ui/issues/212", + "title": "could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "jianchang512/ChatTTS-ui", + "created": "2024-08-07T04:22:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/openads-project/openads-project.github.io/issues/16", + "title": "nvidia-ctk missing from requirements", + "repo": "openads-project/openads-project.github.io", + "created": "2026-08-14T13:56:45Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/n0k0m3/pyspark-notebook-deltalake-docker/issues/8", + "title": "Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]]", + "repo": "n0k0m3/pyspark-notebook-deltalake-docker", + "created": "2023-10-01T19:55:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tyson-swetnam/cloudcompare-docker/issues/3", + "title": "could not select device driver - Kubuntu 22.04", + "repo": "tyson-swetnam/cloudcompare-docker", + "created": "2024-05-26T20:09:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Neo-Diamons/Docker-AI-Webui/issues/1", + "title": "Error response from daemon: could not select device driver \"nvidia\"", + "repo": "Neo-Diamons/Docker-AI-Webui", + "created": "2024-04-28T20:24:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/devnen/Chatterbox-TTS-Server/issues/15", + "title": "Error response from daemon: could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "devnen/Chatterbox-TTS-Server", + "created": "2025-06-07T10:20:03Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/lkwq007/stablediffusion-infinity/issues/25", + "title": "Error response from daemon: could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "lkwq007/stablediffusion-infinity", + "created": "2022-09-25T04:40:06Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/automateyournetwork/packet_buddy/issues/9", + "title": "Error response from daemon: could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "automateyournetwork/packet_buddy", + "created": "2025-03-23T18:05:45Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/FMXExpress/AI-Translate/issues/1", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "FMXExpress/AI-Translate", + "created": "2024-01-10T13:30:31Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/AIR-DISCOVER/ICRA-RM-Sim2Real/issues/20", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "AIR-DISCOVER/ICRA-RM-Sim2Real", + "created": "2022-02-20T14:00:15Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Huanshere/VideoLingo/issues/442", + "title": "(HTTP code 500) server error - could not select device driver \"\" with capabilities: [[gpu]]", + "repo": "Huanshere/VideoLingo", + "created": "2025-04-29T09:39:18Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/SortAnon/ControllableTalkNet/issues/31", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "SortAnon/ControllableTalkNet", + "created": "2023-02-04T20:01:14Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/danchitnis/sycl-container/issues/1", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "danchitnis/sycl-container", + "created": "2023-01-10T20:28:57Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/getumbrel/llama-gpt/issues/150", + "title": "Can't use GPU: could not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "getumbrel/llama-gpt", + "created": "2024-02-24T14:44:53Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/soulteary/docker-llama2-chat/issues/22", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "soulteary/docker-llama2-chat", + "created": "2023-09-12T09:41:53Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/NVlabs/stylegan2-ada-pytorch/issues/227", + "title": "docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "NVlabs/stylegan2-ada-pytorch", + "created": "2022-03-26T10:17:41Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/FoundationVision/ByteTrack/issues/302", + "title": "Docker-Error: could not select device driver with GPU capabilities", + "repo": "FoundationVision/ByteTrack", + "created": "2023-01-02T09:32:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/netease-youdao/QAnything/issues/93", + "title": "[BUG] ", + "repo": "netease-youdao/QAnything", + "created": "2024-01-31T16:43:39Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/kineticsystem/find-me-a-freaking-job/issues/3", + "title": "Compose requires the NVIDIA runtime and host networking even when the model server runs outside the container", + "repo": "kineticsystem/find-me-a-freaking-job", + "created": "2026-09-20T16:03:03Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/jianchang512/ChatTTS-ui/issues/68", + "title": "\u62a5\u9519\u89e3\u51b3\u65b9\u6848\uff1acould not select device driver \"nvidia\" with capabilities: [[gpu]]", + "repo": "jianchang512/ChatTTS-ui", + "created": "2024-06-03T13:16:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/qixuema/envs/issues/25", + "title": "Error response from daemon: Cannot restart container xxx: could not select device driver \"\" with capabilities: [[gpu]]", + "repo": "qixuema/envs", + "created": "2024-07-05T12:25:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nikmcfly/MiroFish-Offline/issues/24", + "title": "Does it work on mac book pro?", + "repo": "nikmcfly/MiroFish-Offline", + "created": "2026-04-01T18:22:55Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/wilicc/gpu-burn/issues/93", + "title": "Docker run fails: docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]]", + "repo": "wilicc/gpu-burn", + "created": "2023-12-02T10:56:58Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/huan/docker-wechat/issues/173", + "title": "3.3.0.115 docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "huan/docker-wechat", + "created": "2022-05-06T20:33:29Z", + "state": "open", + "comments": 13, + "verified": true + }, + { + "url": "https://github.com/replicate/cog-stable-diffusion/issues/12", + "title": "AMD GPU on Ubuntu - docker: Error response from daemon: could not select device driver \"\" with capabilities: [[gpu]].", + "repo": "replicate/cog-stable-diffusion", + "created": "2022-08-31T13:47:41Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 457 +} \ No newline at end of file diff --git a/bench/data/image--pull-access-denied.json b/bench/data/image--pull-access-denied.json new file mode 100644 index 0000000..4d49910 --- /dev/null +++ b/bench/data/image--pull-access-denied.json @@ -0,0 +1,280 @@ +{ + "category": "image: pull access denied", + "rule": "image.exists", + "query": "\"pull access denied\" \"may require\" is:issue created:>=2021-01-01", + "total": 3730, + "sample": [ + { + "url": "https://github.com/PeerDB-io/peerdb/issues/4843", + "title": "[Bug]: minio/minio image no longer available on Docker Hub", + "repo": "PeerDB-io/peerdb", + "created": "2026-09-23T07:54:57Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Dokploy/dokploy/issues/5469", + "title": "Docker source: \"Login Succeeded\" then \"pull access denied\" \u2014 Registry URL never reaches the image name", + "repo": "Dokploy/dokploy", + "created": "2026-09-16T12:45:29Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/confluentinc/demo-siem-optimization/issues/30", + "title": "pull access denied for localbuild/zeek-tcpreplay-kafka, repository does not exist or may require 'docker login'", + "repo": "confluentinc/demo-siem-optimization", + "created": "2026-07-10T04:06:23Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ariadata/qbittorrent-warp/issues/1", + "title": "Pull Access Denied", + "repo": "ariadata/qbittorrent-warp", + "created": "2025-11-17T06:47:50Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/irods/irods_capability_automated_ingest/issues/325", + "title": "GitHub Action Workflow cannot pull minio docker image", + "repo": "irods/irods_capability_automated_ingest", + "created": "2026-09-15T13:54:07Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/EpicGames/lore/issues/208", + "title": "Integration tests failed because minio/minio:latest is no longer available", + "repo": "EpicGames/lore", + "created": "2026-09-15T06:37:53Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/aneisch/thermostat_mqtt_docker/issues/1", + "title": "pull access denied", + "repo": "aneisch/thermostat_mqtt_docker", + "created": "2021-08-20T23:19:38Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/lg/emugaming/issues/3", + "title": "pull access denied ", + "repo": "lg/emugaming", + "created": "2022-07-04T09:27:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/thorrak/fermentrack/issues/783", + "title": "Manual install unable to find an initialized, responsive instance of Fermentrack", + "repo": "thorrak/fermentrack", + "created": "2026-08-28T00:39:01Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dusty-nv/jetson-containers/issues/1707", + "title": "build fails with pull access denied", + "repo": "dusty-nv/jetson-containers", + "created": "2026-06-09T05:37:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/stump-wtf/cairn/issues/2", + "title": "docker-compose.prod.yml: minio/minio:latest is no longer pullable from Docker Hub (use quay.io/minio/minio)", + "repo": "stump-wtf/cairn", + "created": "2026-09-22T19:44:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/victoradrianjimenez/dockerized-zotero/issues/6", + "title": "\"pull access denied for ...\" messages", + "repo": "victoradrianjimenez/dockerized-zotero", + "created": "2025-11-05T17:16:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/containerd/containerd/issues/11103", + "title": "pull access denied, repository does not exist or may require authorization: authorization failed: no basic auth credentials", + "repo": "containerd/containerd", + "created": "2024-12-05T10:34:19Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/coollabsio/coolify/issues/11855", + "title": "[Bug]: Supabase deployment/restart fails because minio/mc image cannot be pulled", + "repo": "coollabsio/coolify", + "created": "2026-09-16T12:08:11Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/rustfs/cli/issues/380", + "title": "Docker usage fails with \"login required\"", + "repo": "rustfs/cli", + "created": "2026-09-21T22:19:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DBRepo-Project/dbrepo/issues/97", + "title": "The install script fails to pull the docker images", + "repo": "DBRepo-Project/dbrepo", + "created": "2026-08-17T11:48:46Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/hcengineering/huly-selfhost/issues/335", + "title": "MinIO removed from Docker Huber - can't finish setup", + "repo": "hcengineering/huly-selfhost", + "created": "2026-09-22T17:35:01Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/cies/low-code-backend-dockered/issues/2", + "title": "Hasura pull access denied", + "repo": "cies/low-code-backend-dockered", + "created": "2022-10-19T15:07:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/0xPolygon/zkevm-node/issues/3771", + "title": "pull access denied zkevm-node", + "repo": "0xPolygon/zkevm-node", + "created": "2024-08-17T21:21:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/sigil-enterprises/ducklake/issues/110", + "title": "MinIO images are gone from Docker Hub: scripts/docker-compose.yml must re-point at quay.io", + "repo": "sigil-enterprises/ducklake", + "created": "2026-09-12T09:17:53Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/epilif1017a/big-data-open-os/issues/2", + "title": "pull access denied for hadoop-base", + "repo": "epilif1017a/big-data-open-os", + "created": "2021-11-16T19:38:57Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/o2ac/o2ac-ur/issues/4", + "title": "o2ac-ur pull access denied", + "repo": "o2ac/o2ac-ur", + "created": "2021-11-18T05:04:11Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/nektos/act/issues/2585", + "title": "Pull access denied, repository does not exist or may require 'docker login'", + "repo": "nektos/act", + "created": "2024-12-24T06:18:05Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/sidpalas/mern-docker-compose/issues/1", + "title": "pull access denied for react-app", + "repo": "sidpalas/mern-docker-compose", + "created": "2022-12-30T13:38:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/trevezani/kong-consul-springboot/issues/1", + "title": "Pull access denied for census-zipcode", + "repo": "trevezani/kong-consul-springboot", + "created": "2021-02-04T13:18:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/indrz/indrz-be/issues/129", + "title": "Pull access denied for indrz/gogse", + "repo": "indrz/indrz-be", + "created": "2021-09-22T11:51:14Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/OpenSourcePolitics/decidim-ditp/issues/4", + "title": "pull access denied for decidim-app", + "repo": "OpenSourcePolitics/decidim-ditp", + "created": "2022-06-17T12:40:16Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/AchimPieters/ESP-HomeKit-SDK-Revised-Installation/issues/1", + "title": "pull access denied for esp-sdk", + "repo": "AchimPieters/ESP-HomeKit-SDK-Revised-Installation", + "created": "2021-10-31T17:21:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/HKUDS/AI-Researcher/issues/80", + "title": "Bug: Docker image 'tjbtech1/airesearcher:v1' pull access denied", + "repo": "HKUDS/AI-Researcher", + "created": "2025-12-25T12:29:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/swiftlang/swift-org-website/issues/1346", + "title": "Run OpenAPI Tester: Pull access denied for swift-website-builder", + "repo": "swiftlang/swift-org-website", + "created": "2026-03-06T19:30:21Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 3730 +} \ No newline at end of file diff --git a/bench/data/image--tag-repo-not-found.json b/bench/data/image--tag-repo-not-found.json new file mode 100644 index 0000000..44b7e47 --- /dev/null +++ b/bench/data/image--tag-repo-not-found.json @@ -0,0 +1,280 @@ +{ + "category": "image: tag/repo not found", + "rule": "image.exists", + "query": "\"manifest unknown\" docker is:issue created:>=2021-01-01", + "total": 7405, + "sample": [ + { + "url": "https://github.com/openwhistle/OpenWhistle/issues/69", + "title": "Docker image - manifest unknown", + "repo": "openwhistle/OpenWhistle", + "created": "2026-09-22T09:24:54Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/memiiso/debezium-server-iceberg/issues/744", + "title": "docker version 1.1.0.Final manifest unknown", + "repo": "memiiso/debezium-server-iceberg", + "created": "2026-09-10T11:21:09Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/metadatastician/paint-type/issues/88", + "title": "Sustainability Analysis (OikosBot): pinned container digest returns manifest unknown", + "repo": "metadatastician/paint-type", + "created": "2026-09-22T21:57:24Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/decolua/9router/issues/4167", + "title": "Docker image decolua/9router:0.5.81 is not available", + "repo": "decolua/9router", + "created": "2026-09-19T07:06:20Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/wazuh/wazuh/issues/38861", + "title": "Wazuh 5.0.0 beta docker manifest unknown - EDIT : wrong image names in docs", + "repo": "wazuh/wazuh", + "created": "2026-09-03T15:38:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/CoreNovaLabs/CoreNovaLaunch/issues/22", + "title": "verify(drawio): unknown FAILED (MANUAL_REQUIRED)", + "repo": "CoreNovaLabs/CoreNovaLaunch", + "created": "2026-09-23T20:47:49Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/marcopiovanello/yt-dlp-web-ui/issues/379", + "title": "v4 Docker images missing", + "repo": "marcopiovanello/yt-dlp-web-ui", + "created": "2026-07-25T07:05:25Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/CoreNovaLabs/CoreNovaLaunch/issues/10", + "title": "verify(ghost): unknown FAILED (MANUAL_REQUIRED)", + "repo": "CoreNovaLabs/CoreNovaLaunch", + "created": "2026-09-08T20:25:25Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/conversun/fnos-apps/issues/306", + "title": "[Bug]", + "repo": "conversun/fnos-apps", + "created": "2026-09-16T09:58:37Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ublue-os/bazzite/issues/4764", + "title": "Rebase to version 44.20260429 fails with \"manifest unknown\" error", + "repo": "ublue-os/bazzite", + "created": "2026-04-29T08:53:30Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/jeremy-albinet/Stitch-Revitalized-For-Roku/issues/152", + "title": "Error manifest unknown when pulling ghcr.io/jeremy-albinet/fmp4-demux-proxy:main", + "repo": "jeremy-albinet/Stitch-Revitalized-For-Roku", + "created": "2026-07-20T21:03:00Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/golusoris/golusoris/issues/563", + "title": "build(ai/tiny): publish digest-pinnable Gemma and LiteRT trainer images", + "repo": "golusoris/golusoris", + "created": "2026-09-19T20:10:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dniku/free-lunch-saliency/issues/1", + "title": "Docker Image manifest unknown", + "repo": "dniku/free-lunch-saliency", + "created": "2022-02-22T21:28:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Softcatala/whisper-ctranslate2/issues/148", + "title": "docker image broken? workaround provided", + "repo": "Softcatala/whisper-ctranslate2", + "created": "2026-07-30T11:07:33Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/safak/microservices-ecommerce/issues/3", + "title": "manifest for bitnami/kafka:latest not found: manifest unknown: manifest unknown", + "repo": "safak/microservices-ecommerce", + "created": "2025-10-07T13:26:07Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/elfhosted/newtarr/issues/16", + "title": "Missing Docker Image", + "repo": "elfhosted/newtarr", + "created": "2026-07-02T14:40:52Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/k8snetworkplumbingwg/multus-cni/issues/1551", + "title": "v4.3.1 thin container image was not published", + "repo": "k8snetworkplumbingwg/multus-cni", + "created": "2026-09-20T09:48:17Z", + "state": "open", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/nextbsd/nextbsd/issues/423", + "title": "CI: toolchain dispatches downstream from the amd64 matrix leg, racing the arm64 image push (manifest unknown)", + "repo": "nextbsd/nextbsd", + "created": "2026-08-27T02:46:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DamianB-BitFlipper/proton-drive-sync/issues/127", + "title": "Can't pull docker image", + "repo": "DamianB-BitFlipper/proton-drive-sync", + "created": "2026-03-07T13:16:45Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/bgshih/crnn/issues/159", + "title": "manifest unknown -> manifest for kaixhin/cuda-torch:latest not found: manifest unknown: manifest unknown", + "repo": "bgshih/crnn", + "created": "2021-04-29T08:54:23Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RedHatQuickCourses/rhdh-idp/issues/7", + "title": "Module 2: Build fails: error: build error: failed to pull image: openjdk:17", + "repo": "RedHatQuickCourses/rhdh-idp", + "created": "2026-09-14T16:49:58Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/tuna-os/xfce-linux/issues/145", + "title": "[shared-ci] base image ghcr.io/tuna-os/xfce-linux:latest manifest unknown \u2014 breaks E2E on main and PRs", + "repo": "tuna-os/xfce-linux", + "created": "2026-09-03T04:11:31Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/conversun/fnos-apps/issues/289", + "title": "[Bug]", + "repo": "conversun/fnos-apps", + "created": "2026-09-02T11:37:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/conversun/fnos-apps/issues/288", + "title": "[Bug]", + "repo": "conversun/fnos-apps", + "created": "2026-09-02T07:43:40Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/ben1234560/k8s_PaaS/issues/52", + "title": "Using default tag: latest Error response from daemon: manifest for kubernetes/pause:latest not found: manifest unknown: manifest unknown", + "repo": "ben1234560/k8s_PaaS", + "created": "2024-08-17T11:57:51Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/mehmetgoren/feniks/issues/12", + "title": "docker-compose occurs error that manifest unknown", + "repo": "mehmetgoren/feniks", + "created": "2023-05-04T07:07:47Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/christiaandouma/fronius-auth-proxy/issues/17", + "title": "Addon will not install on HA 2026.8.0 - deprecated methods", + "repo": "christiaandouma/fronius-auth-proxy", + "created": "2026-08-07T16:12:36Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/BreachSAFE/qureddy/issues/540", + "title": "docs: consolidate README \u00a71 Docker examples + add Docker Hub rate-limit guidance", + "repo": "BreachSAFE/qureddy", + "created": "2026-08-31T01:28:37Z", + "state": "open", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/imdingtalk/image-mirror/issues/1854", + "title": "kubewatch/kubewatch:2.16.1 (\u5171 1 \u4e2a\u955c\u50cf)", + "repo": "imdingtalk/image-mirror", + "created": "2026-09-18T07:57:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/sickcodes/Docker-OSX/issues/889", + "title": "Error response from daemon: manifest for sickcodes/docker-osx:naked not found: manifest unknown: manifest unknown", + "repo": "sickcodes/Docker-OSX", + "created": "2025-06-25T04:07:51Z", + "state": "open", + "comments": 6, + "verified": true + } + ], + "precision": 0.9333333333333333, + "adjusted": 6911 +} \ No newline at end of file diff --git a/bench/data/kernel--vm-max-map-count.json b/bench/data/kernel--vm-max-map-count.json new file mode 100644 index 0000000..d896ff4 --- /dev/null +++ b/bench/data/kernel--vm-max-map-count.json @@ -0,0 +1,280 @@ +{ + "category": "kernel: vm.max_map_count", + "rule": "kernel.sysctl", + "query": "\"vm.max_map_count\" \"is too low\" is:issue created:>=2021-01-01", + "total": 369, + "sample": [ + { + "url": "https://github.com/memgraph/memgraph/issues/3881", + "title": "v3.8.x hard-exits when vm.max_map_count < 262144 in IN_MEMORY_ANALYTICAL mode", + "repo": "memgraph/memgraph", + "created": "2026-03-10T04:06:02Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/isejalabs/homelab/issues/134", + "title": "Mongo startup message `vm.max_map_count is too low`", + "repo": "isejalabs/homelab", + "created": "2024-12-27T11:28:02Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/seonwoojh/TIL/issues/4", + "title": "[elasticsearch] max virtual memory areas vm.max_map_count [65530] is too low", + "repo": "seonwoojh/TIL", + "created": "2022-07-07T00:59:59Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/elastic/elasticsearch/issues/86634", + "title": "Max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]", + "repo": "elastic/elasticsearch", + "created": "2022-05-10T18:22:04Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/wazuh/wazuh-docker/issues/2403", + "title": "Issues docker compose multi node possible solutions", + "repo": "wazuh/wazuh-docker", + "created": "2026-05-18T06:10:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/questdb/documentation/issues/370", + "title": "Update /deployment/docker", + "repo": "questdb/documentation", + "created": "2026-02-18T11:33:29Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/3450", + "title": "[BUG] `/proc/sys/vm/max_map_count` from host is not leveraged for running containers", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2022-11-23T18:13:25Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/MarquezProject/marquez/issues/3025", + "title": "Docker - OpenSearch Installation Documentation", + "repo": "MarquezProject/marquez", + "created": "2025-01-30T03:41:12Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/opensearch-project/OpenSearch/issues/6925", + "title": "TroubleShooting Guide for students, later to transfer for DEVELOPER.md as single source of truth ", + "repo": "opensearch-project/OpenSearch", + "created": "2023-04-01T07:09:52Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/memgraph/memgraph/issues/3607", + "title": "Data persistence issue when deploying Memgraph with Docker on Railway - seeking backup/restore solutions", + "repo": "memgraph/memgraph", + "created": "2026-01-04T21:52:09Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/aprenderdevops/docker-sonarqube/issues/1", + "title": "Sonarqube container error", + "repo": "aprenderdevops/docker-sonarqube", + "created": "2022-05-03T15:05:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Enigma-IIITS/enigma-site/issues/7", + "title": "explore and try to fix Mongo db startup warnings in Digital Ocean VM", + "repo": "Enigma-IIITS/enigma-site", + "created": "2024-02-24T10:57:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/opensearch-project/project-website/issues/354", + "title": "[BUG] Suggested docker-compose.yml doesn't work with Docker Desktop for Windows, WSL2 mode", + "repo": "opensearch-project/project-website", + "created": "2021-10-01T14:42:54Z", + "state": "open", + "comments": 17, + "verified": true + }, + { + "url": "https://github.com/Tae4an/Smart-Office/issues/28", + "title": "ELK Stack \ud658\uacbd \uc124\uc815 \ubc0f \uc77c\ubc18\uc801\uc778 \ubb38\uc81c \ud574\uacb0 \uac00\uc774\ub4dc", + "repo": "Tae4an/Smart-Office", + "created": "2024-11-20T13:39:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ddev/ddev-elasticsearch/issues/11", + "title": "[question]: Elastic Enterprise Search", + "repo": "ddev/ddev-elasticsearch", + "created": "2023-07-18T01:58:34Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/bryanlittlefield/TUGBOAT/issues/82", + "title": "Add Elastic Search Container + Configs", + "repo": "bryanlittlefield/TUGBOAT", + "created": "2022-06-09T16:22:17Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3759", + "title": "TroubleShooting Guide for students, later to transfer for DEVELOPER Guide.md as single source of truth", + "repo": "opensearch-project/OpenSearch-Dashboards", + "created": "2023-03-31T21:51:44Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/r-universe-org/help/issues/509", + "title": "MongoDB 8 server warnings", + "repo": "r-universe-org/help", + "created": "2024-10-10T13:15:45Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/rkaehdaos/docker-elk/issues/14", + "title": "fix error", + "repo": "rkaehdaos/docker-elk", + "created": "2022-03-24T01:55:51Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/arangodb/kube-arangodb/issues/1303", + "title": "Setup initContainer for recommended sysctls", + "repo": "arangodb/kube-arangodb", + "created": "2023-04-28T12:25:52Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/elastic/elasticsearch/issues/101401", + "title": "Fix docker compose default \"Getting Started\"", + "repo": "elastic/elasticsearch", + "created": "2023-10-26T18:48:34Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/713", + "title": "Increase open file limit in the VM / inside containers", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2021-10-01T16:12:18Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/anaer/blog/issues/169", + "title": "elasticsearch-7.6.2\u5b89\u88c5", + "repo": "anaer/blog", + "created": "2025-11-11T07:59:21Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/soon530/data-study/issues/31", + "title": "MongoDB \u4e92\u52d5\u4e00\u4e0b", + "repo": "soon530/data-study", + "created": "2022-11-04T09:04:34Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/UW-COSMOS/Cosmos/issues/167", + "title": "Elasticsearch virtual memory error", + "repo": "UW-COSMOS/Cosmos", + "created": "2022-08-12T08:20:37Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/community-scripts/ProxmoxVED/issues/2139", + "title": "Elasticsearch", + "repo": "community-scripts/ProxmoxVED", + "created": "2026-08-04T06:36:37Z", + "state": "closed", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/aws/containers-roadmap/issues/1452", + "title": "[Fargate] [request]: EKS Fargate not allowing to set system limits on mmap counts require to run Elasticsearch", + "repo": "aws/containers-roadmap", + "created": "2021-07-27T12:36:36Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/Mininglamp-OSS/octo-deployment/issues/179", + "title": "feat: add vm.max_map_count preflight check before starting search profile (OpenSearch)", + "repo": "Mininglamp-OSS/octo-deployment", + "created": "2026-08-13T08:09:48Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/nestybox/sysbox/issues/351", + "title": "EPERM during setrlimit() execution prevents apps from initializing", + "repo": "nestybox/sysbox", + "created": "2021-07-14T23:50:47Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/agentscope-ai/AgentTeams/issues/888", + "title": "v1.1.2 ERROR: Higress Gateway not ready after 120s", + "repo": "agentscope-ai/AgentTeams", + "created": "2026-05-30T18:52:00Z", + "state": "open", + "comments": 11, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 369 +} \ No newline at end of file diff --git a/bench/data/memory--mssql-minimum.json b/bench/data/memory--mssql-minimum.json new file mode 100644 index 0000000..15f7f54 --- /dev/null +++ b/bench/data/memory--mssql-minimum.json @@ -0,0 +1,280 @@ +{ + "category": "memory: mssql minimum", + "rule": "resources.memory", + "query": "\"requires a machine with at least 2000 megabytes\" is:issue created:>=2021-01-01", + "total": 4653, + "sample": [ + { + "url": "https://github.com/ultralytics/yolov5/issues/2000", + "title": "Live streaming after Cloud Engine", + "repo": "ultralytics/yolov5", + "created": "2021-01-21T04:46:17Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/ultrabug/py3status/issues/2000", + "title": "`window` does not work under sway", + "repo": "ultrabug/py3status", + "created": "2021-01-18T19:19:09Z", + "state": "closed", + "comments": 4, + "verified": false + }, + { + "url": "https://github.com/Waiviogit/waivio/issues/2000", + "title": "Remove the intermediate screen waivio when loading the site", + "repo": "Waiviogit/waivio", + "created": "2021-02-10T09:14:50Z", + "state": "closed", + "comments": 2, + "verified": false + }, + { + "url": "https://github.com/actesrun/appcenter-alerting-test-int/issues/2000", + "title": "Fix System.IO.IOException: Network down in Crash Group # 4054616594u", + "repo": "actesrun/appcenter-alerting-test-int", + "created": "2021-08-26T10:29:29Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/KadeArchive/Kade-Engine/issues/2000", + "title": "Chart Speed Bug", + "repo": "KadeArchive/Kade-Engine", + "created": "2021-08-26T13:03:09Z", + "state": "open", + "comments": 2, + "verified": false + }, + { + "url": "https://github.com/type-challenges/type-challenges/issues/2000", + "title": "14 - First of Array", + "repo": "type-challenges/type-challenges", + "created": "2021-06-28T11:19:30Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/DarkPacks/SkyFactory-4/issues/2000", + "title": "SK4 v4.2.2 crashes on SSP world-load ; `IllegalStateException: failed to create a child event loop`", + "repo": "DarkPacks/SkyFactory-4", + "created": "2021-11-20T10:06:10Z", + "state": "open", + "comments": 3, + "verified": false + }, + { + "url": "https://github.com/julia-vscode/julia-vscode/issues/2000", + "title": "Use VS-Code integrated terminal insted of VS-Code system terminal", + "repo": "julia-vscode/julia-vscode", + "created": "2021-03-06T03:30:05Z", + "state": "closed", + "comments": 9, + "verified": false + }, + { + "url": "https://github.com/tarantool/doc/issues/2000", + "title": "[1pt] Give link from Getting started with Cartridge to the Cartridge docs", + "repo": "tarantool/doc", + "created": "2021-03-26T08:02:06Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/swig/swig/issues/2000", + "title": "Swig go not working with output char * argument", + "repo": "swig/swig", + "created": "2021-04-22T08:55:28Z", + "state": "closed", + "comments": 2, + "verified": false + }, + { + "url": "https://github.com/espruino/Espruino/issues/2000", + "title": "advertising broken with S140 >= 6.1.0", + "repo": "espruino/Espruino", + "created": "2021-04-21T22:48:13Z", + "state": "closed", + "comments": 12, + "verified": false + }, + { + "url": "https://github.com/wandb/wandb/issues/2000", + "title": "[Feature] Add support for running Wandb inside of a celery task", + "repo": "wandb/wandb", + "created": "2021-03-24T19:40:47Z", + "state": "closed", + "comments": 2, + "verified": false + }, + { + "url": "https://github.com/mainIine/foe-helfer-extension/issues/2000", + "title": "Bug: Menu Content settings with right menu not aligned correctly", + "repo": "mainIine/foe-helfer-extension", + "created": "2021-05-05T08:30:03Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/bigcommerce/cornerstone/issues/2000", + "title": "concat combined with json is returning an object with a string parameter", + "repo": "bigcommerce/cornerstone", + "created": "2021-02-28T03:39:30Z", + "state": "closed", + "comments": 3, + "verified": false + }, + { + "url": "https://github.com/airsdk/Adobe-Runtime-Support/issues/2000", + "title": "Dealing with emojis \ud83d\ude0c", + "repo": "airsdk/Adobe-Runtime-Support", + "created": "2022-07-01T11:32:55Z", + "state": "open", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/aceblockID/monitoring/issues/2000", + "title": "\ud83d\uded1 AceBlock Swagger UI is down", + "repo": "aceblockID/monitoring", + "created": "2022-07-16T14:27:18Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/dapr/components-contrib/issues/2000", + "title": "[redis Configuration Api]The specified redisDB should be supported", + "repo": "dapr/components-contrib", + "created": "2022-08-23T05:54:53Z", + "state": "open", + "comments": 7, + "verified": false + }, + { + "url": "https://github.com/xlwings/xlwings/issues/2000", + "title": "pivot_item.visible does not work ", + "repo": "xlwings/xlwings", + "created": "2022-08-26T06:21:23Z", + "state": "closed", + "comments": 4, + "verified": false + }, + { + "url": "https://github.com/kubernetes-sigs/controller-runtime/issues/2000", + "title": "Add a util method to recognize if resource in reconcile loop is being deleted", + "repo": "kubernetes-sigs/controller-runtime", + "created": "2022-09-13T16:06:04Z", + "state": "closed", + "comments": 7, + "verified": false + }, + { + "url": "https://github.com/FreezingMoon/AncientBeast/issues/2000", + "title": "proper hexagon tile hovering [bounty: 35 XTR]", + "repo": "FreezingMoon/AncientBeast", + "created": "2022-01-01T04:39:15Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/BetaMasaheft/Documentation/issues/2000", + "title": "HTTP ERROR 500 EMML6451", + "repo": "BetaMasaheft/Documentation", + "created": "2022-03-10T15:58:35Z", + "state": "closed", + "comments": 3, + "verified": false + }, + { + "url": "https://github.com/TrueBlocks/trueblocks-core/issues/2000", + "title": "Names: database doesn't populate", + "repo": "TrueBlocks/trueblocks-core", + "created": "2022-01-07T20:21:31Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/javisu/monitor/issues/2000", + "title": "\ud83d\uded1 Joaquin Bau, 5 is down", + "repo": "javisu/monitor", + "created": "2022-06-07T19:52:25Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/incubateur-ademe/territoires-en-transitions/issues/2000", + "title": "Mettre la partie longue du titre en description CAE 5.1.1.1.3", + "repo": "incubateur-ademe/territoires-en-transitions", + "created": "2022-05-23T15:39:56Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/glium/glium/issues/2000", + "title": "clear_color_srgb(red: 0.0,green: 0.0,blue: 0.0,alpha: 0.0) ot working", + "repo": "glium/glium", + "created": "2022-03-19T14:46:26Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/apache/hop/issues/2000", + "title": "[Task]: Cleanup XML of action Wait for SQL", + "repo": "apache/hop", + "created": "2022-12-07T11:07:04Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/withfig/fig/issues/2000", + "title": "bug: Autocomplete pupup always in top left corner of Alacritty", + "repo": "withfig/fig", + "created": "2022-11-06T09:17:10Z", + "state": "closed", + "comments": 12, + "verified": false + }, + { + "url": "https://github.com/devfsa/vagas/issues/2000", + "title": "[Remoto] Frontend Angular Developer na Coodesh", + "repo": "devfsa/vagas", + "created": "2022-10-28T15:20:19Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/Oleksandra2/QAGreen/issues/2000", + "title": "Tutorial Page abap-environment-create-service-consumption-model.md Issue. LOCAL", + "repo": "Oleksandra2/QAGreen", + "created": "2023-01-19T05:02:00Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/tahowallet/extension/issues/2000", + "title": "Gwei gas prices are not updating live on Optimism", + "repo": "tahowallet/extension", + "created": "2022-08-22T13:45:59Z", + "state": "closed", + "comments": 3, + "verified": false + } + ], + "precision": 0.0, + "adjusted": 0 +} \ No newline at end of file diff --git a/bench/data/memory--oomkilled.json b/bench/data/memory--oomkilled.json new file mode 100644 index 0000000..de24fa4 --- /dev/null +++ b/bench/data/memory--oomkilled.json @@ -0,0 +1,280 @@ +{ + "category": "memory: OOMKilled", + "rule": "resources.memory", + "query": "OOMKilled \"exit code 137\" is:issue created:>=2021-01-01", + "total": 97607, + "sample": [ + { + "url": "https://github.com/devmahmoudi/laravel-todolist/issues/76", + "title": "resolve Docker process exit code 137 in GitHub Actions CI/CD pipeline", + "repo": "devmahmoudi/laravel-todolist", + "created": "2025-11-10T09:13:51Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/kestra-io/plugin-kubernetes/issues/339", + "title": "io.kestra.plugin.kubernetes.PodCreate Fails with Exit Code 137 Despite Resource Limits", + "repo": "kestra-io/plugin-kubernetes", + "created": "2025-06-10T09:56:28Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/Jersyfi/taktus/issues/29", + "title": "The container adapter's memory-kill test is flaky: the engine does not always report OOMKilled", + "repo": "Jersyfi/taktus", + "created": "2026-09-23T12:52:31Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/apache/airflow/issues/73164", + "title": "KubernetesExecutor: Event log says \"Pod failed because of None\" for OOMKilled pods", + "repo": "apache/airflow", + "created": "2026-09-15T06:42:02Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/KDM-cli/kdm-cli/issues/275", + "title": "[v4.0.0] Phase 4: Deterministic Kubernetes/Docker Failure Rule Engine", + "repo": "KDM-cli/kdm-cli", + "created": "2026-09-10T22:48:57Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/open-telemetry/opentelemetry-demo/issues/3811", + "title": "[prometheus] Default Helm memory limit can cause an OOMKilled restart loop", + "repo": "open-telemetry/opentelemetry-demo", + "created": "2026-08-06T03:21:53Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/MaRDI4NFDI/MaRDIRoadmap/issues/235", + "title": "qlever-updater OOMKilled crash loop due to insufficient memory limit", + "repo": "MaRDI4NFDI/MaRDIRoadmap", + "created": "2026-09-17T07:33:09Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/world-federation-of-advertisers/cross-media-measurement/issues/4500", + "title": "edp-aggregator API servers are OOMKilled under VID-labeling load, hanging edpa-cloud-test", + "repo": "world-federation-of-advertisers/cross-media-measurement", + "created": "2026-09-12T14:33:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/AstrBotDevs/shipyard-neo/issues/32", + "title": "Warm Pool \u6b63\u5e38\u8f6e\u6362\u65f6\u5f3a\u5236\u5220\u9664\u8fd0\u884c\u4e2d\u5bb9\u5668\uff0c\u5bfc\u81f4 SIGKILL / Exit Code 137", + "repo": "AstrBotDevs/shipyard-neo", + "created": "2026-09-05T14:22:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/zyra-project/zyra/issues/153", + "title": "`visualize-timeseries` crashes with OOM (exit code 137) in headless/Kubernetes environments", + "repo": "zyra-project/zyra", + "created": "2026-04-27T20:38:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/achourisoufiene/cylmaaaaaaaaa/issues/1", + "title": "[Test] OOMKilled error in Docker container", + "repo": "achourisoufiene/cylmaaaaaaaaa", + "created": "2026-06-08T23:20:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/BarminGeorge/ShuKnow/issues/137", + "title": "\u0423\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u0435: \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442 \u043e\u0442\u0432\u0435\u0442\u0430 \u0438\u0438 \u0432 \u0447\u0430\u0442\u0435", + "repo": "BarminGeorge/ShuKnow", + "created": "2026-04-24T07:33:55Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/ChatLab/ChatLab/issues/137", + "title": "\u5916\u90e8\u57df\u540d\u8bf7\u6c42\u5b58\u5728\u5b89\u5168\u98ce\u9669", + "repo": "ChatLab/ChatLab", + "created": "2026-04-13T12:52:11Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/parkerhancock/patent_client/issues/137", + "title": "\"Event Loop is closed\"/Deadlocks for simultaneous requests", + "repo": "parkerhancock/patent_client", + "created": "2024-01-10T17:10:06Z", + "state": "closed", + "comments": 4, + "verified": false + }, + { + "url": "https://github.com/ImAdrenalize/qkd-digital-twin-project/issues/137", + "title": "Development environment", + "repo": "ImAdrenalize/qkd-digital-twin-project", + "created": "2026-04-04T20:30:31Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/modelcontextprotocol/registry/issues/137", + "title": "Document convention: server.json presence in package artifact", + "repo": "modelcontextprotocol/registry", + "created": "2025-06-13T13:09:59Z", + "state": "closed", + "comments": 6, + "verified": false + }, + { + "url": "https://github.com/ITU-DevOps2026/ITU-minitwit/issues/137", + "title": "Refactor - Function that fetches own timeline is very slow", + "repo": "ITU-DevOps2026/ITU-minitwit", + "created": "2026-04-11T19:40:12Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/okdevme/DiscordPlugins/issues/137", + "title": "ChannelsPreview error with new update", + "repo": "okdevme/DiscordPlugins", + "created": "2025-09-26T15:45:53Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/knishioka/ib-sec-mcp/issues/137", + "title": "bug(validators): validate_isin rejects valid ISINs (Luhn check digit bug)", + "repo": "knishioka/ib-sec-mcp", + "created": "2026-06-04T06:24:10Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/PRX/The-World-FE-Nextjs/issues/137", + "title": "Opt website out of Google FLoC", + "repo": "PRX/The-World-FE-Nextjs", + "created": "2021-04-29T16:46:50Z", + "state": "closed", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/hasherwi/Archipelago-kirbyam/issues/137", + "title": "Refactor KirbyAM address schema: separate AP transport mailbox from native game-state flags", + "repo": "hasherwi/Archipelago-kirbyam", + "created": "2026-03-19T15:50:59Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/margaritauk/home-offer-direct/issues/137", + "title": "As a buyer, I want to manually update my offer status when the agent responds so that my dashboard reflects reality", + "repo": "margaritauk/home-offer-direct", + "created": "2026-06-03T19:10:12Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/gocronx-team/gocron/issues/137", + "title": "feat: task template management, script versioning, and code editor", + "repo": "gocronx-team/gocron", + "created": "2026-04-17T00:45:52Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/ix-infrastructure/Ix/issues/137", + "title": "Add R parser support (.r, .R)", + "repo": "ix-infrastructure/Ix", + "created": "2026-04-03T21:56:12Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/Shaurya1907/PYSCHECARE-main/issues/137", + "title": "docs: CODE_OF_CONDUCT.md Missing", + "repo": "Shaurya1907/PYSCHECARE-main", + "created": "2026-06-05T19:45:18Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/eslint/eslint-github-bot/issues/137", + "title": "shorten the text in the comment, to make less annoying", + "repo": "eslint/eslint-github-bot", + "created": "2021-01-22T03:14:08Z", + "state": "closed", + "comments": 7, + "verified": false + }, + { + "url": "https://github.com/robe070/cookbooks/issues/137", + "title": "Implement AWS JPN images CI pipelines", + "repo": "robe070/cookbooks", + "created": "2021-03-30T03:12:15Z", + "state": "closed", + "comments": 4, + "verified": false + }, + { + "url": "https://github.com/denoland/denokv/issues/137", + "title": "remote: expired metadata token is used without refresh or retry, surfacing auth errors to callers", + "repo": "denoland/denokv", + "created": "2026-06-05T19:35:13Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/plures/pares-radix/issues/137", + "title": "[ci-feedback] Post-merge CI failure at c2cfd14", + "repo": "plures/pares-radix", + "created": "2026-05-14T22:14:21Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/aliendomain/workable/issues/137", + "title": "Remove public id-based system and definition APIs", + "repo": "aliendomain/workable", + "created": "2026-06-06T23:55:08Z", + "state": "closed", + "comments": 0, + "verified": false + } + ], + "precision": 0.36666666666666664, + "adjusted": 35789 +} \ No newline at end of file diff --git a/bench/data/network-not-found.json b/bench/data/network-not-found.json new file mode 100644 index 0000000..21d3f3e --- /dev/null +++ b/bench/data/network-not-found.json @@ -0,0 +1,280 @@ +{ + "category": "network not found", + "rule": "", + "query": "docker \"network\" \"not found\" compose is:issue created:>=2021-01-01", + "total": 18407, + "sample": [ + { + "url": "https://github.com/tmwclaxton/clashofdots/issues/12", + "title": "INC: LGS Pipeline deploy recreate \u2014 Compose temp name not found (2026-09-18)", + "repo": "tmwclaxton/clashofdots", + "created": "2026-09-18T20:22:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tmwclaxton/clashofdots/issues/11", + "title": "INC: LGS Pipeline ctr not found after Compose mid-replace rename (2026-09-18)", + "repo": "tmwclaxton/clashofdots", + "created": "2026-09-18T20:21:23Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Finsys/dockhand/issues/1493", + "title": "[BUG] env file not found with remote host running Hawser", + "repo": "Finsys/dockhand", + "created": "2026-08-30T06:40:19Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/microsoft/vscode/issues/331774", + "title": "Git diff fails with GitFileSystemProvider Repository not found on Windows", + "repo": "microsoft/vscode", + "created": "2026-08-20T10:46:14Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/orkestra-cc/orkestra/issues/300", + "title": "bug: backup.sh reports rustfs as backed up when the S3 sync fails (network not found)", + "repo": "orkestra-cc/orkestra", + "created": "2026-08-26T19:27:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/JoeRegnier/retiree-plan/issues/138", + "title": "Cannot bootstrap myself using Docker", + "repo": "JoeRegnier/retiree-plan", + "created": "2026-09-19T14:40:02Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/treeseed-ai/deployment/issues/369", + "title": "Remove active TreeSeed Compose endpoints before managed networks", + "repo": "treeseed-ai/deployment", + "created": "2026-09-01T03:29:55Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/cameri/schematics/issues/88", + "title": "restrict-docker-api-access: skeleton pins a nonexistent image, tecnativa/restrict-docker-api-access", + "repo": "cameri/schematics", + "created": "2026-09-20T13:14:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/filippolmt/toolbox/issues/969", + "title": "Make the session container's network configurable, and keep local Docker networks off VPN-routed subnets", + "repo": "filippolmt/toolbox", + "created": "2026-09-09T15:48:43Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/OpenSDLC-Dev/managed-agent-platform/issues/467", + "title": "executor: validate SANDBOX_DOCKER_GATE_NETWORK at startup \u2014 a wrong-but-existing gate network fails silently, per session", + "repo": "OpenSDLC-Dev/managed-agent-platform", + "created": "2026-08-23T04:02:28Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Proaxiom-Cyber/guacamole-saml-sso/issues/30", + "title": "Diagnose nginx network loss after port conflict and improve installer recovery", + "repo": "Proaxiom-Cyber/guacamole-saml-sso", + "created": "2026-09-23T04:51:33Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/petercort/chaotic-ecommerce/issues/2", + "title": "[Chaos] S7: JVM Heap Exhaustion \u2014 Docker network not found (stale network name from Java-era compose)", + "repo": "petercort/chaotic-ecommerce", + "created": "2026-04-22T01:06:12Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/unihosted/unifi-os-server-docker/issues/4", + "title": "Network Application not always comming up after host reboot", + "repo": "unihosted/unifi-os-server-docker", + "created": "2026-07-31T10:01:33Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/Finsys/dockhand/issues/1278", + "title": "[BUG] Stack creation from Git fails with external network not found, while local Compose and regular stack creation work", + "repo": "Finsys/dockhand", + "created": "2026-07-11T13:13:18Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/kasmtech/workspaces-issues/issues/673", + "title": "[Bug] - network not found", + "repo": "kasmtech/workspaces-issues", + "created": "2024-12-26T23:11:14Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/docker/desktop-feedback/issues/645", + "title": "docker vmm does not use the docker CLI", + "repo": "docker/desktop-feedback", + "created": "2026-09-01T11:41:13Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/groupsky/homy/issues/1559", + "title": "ci: subnet-overlap guards check example.env only, so a host whose .env differs is unprotected", + "repo": "groupsky/homy", + "created": "2026-09-08T21:33:43Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/vale46n1/immich_duplicate_finder/issues/57", + "title": "Network bridge not found", + "repo": "vale46n1/immich_duplicate_finder", + "created": "2025-01-22T16:30:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/testcontainers/testcontainers-go/issues/2952", + "title": "[Bug]: `panic: rootless Docker not found` with Colima on macos", + "repo": "testcontainers/testcontainers-go", + "created": "2025-01-30T17:18:49Z", + "state": "open", + "comments": 9, + "verified": true + }, + { + "url": "https://github.com/goharbor/harbor/issues/23001", + "title": "CSRF token not found in request", + "repo": "goharbor/harbor", + "created": "2026-03-16T14:44:33Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/smokin-salmon/smoked-salmon/issues/426", + "title": "Spectrals web server not working; `/spectrals` results in 404 not found; welcome page on `/` is served", + "repo": "smokin-salmon/smoked-salmon", + "created": "2026-07-15T04:23:36Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/rtCamp/Frappe-Manager/issues/480", + "title": "Bench nginx races the frappe container at startup; compose templates define no restart policy, depends_on, or healthchecks", + "repo": "rtCamp/Frappe-Manager", + "created": "2026-09-20T18:10:19Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/deepfence/ThreatMapper/issues/2425", + "title": "Docker Compose Deployment - 404 page not found", + "repo": "deepfence/ThreatMapper", + "created": "2025-07-19T15:15:37Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/FullStack-AITech/Online-Shop-Microservices/issues/71", + "title": "Kafka: create topics from a one-shot init container", + "repo": "FullStack-AITech/Online-Shop-Microservices", + "created": "2026-09-23T16:10:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/chatwoot/chatwoot/issues/15404", + "title": "WhatsApp Cloud API and Dualhook: outbound messages from Chatwoot UI fail with `Not found`, while inbound and direct sends work", + "repo": "chatwoot/chatwoot", + "created": "2026-08-10T20:16:48Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/R-s0n/ars0n-framework-v2/issues/117", + "title": "x8 build stage fails during `docker-compose up --build` \u2014 404 on openssl (stale bullseye-security mirror)", + "repo": "R-s0n/ars0n-framework-v2", + "created": "2026-09-12T13:45:38Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/moby/moby/issues/44403", + "title": "Swarm: network none not found", + "repo": "moby/moby", + "created": "2022-11-03T11:53:19Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/achimdehnert/platform/issues/2389", + "title": "deploy.sh: 'compose run --rm' und 'up -d --force-recreate' konkurrieren um das Projekt-Netz", + "repo": "achimdehnert/platform", + "created": "2026-08-27T12:41:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/UNITRONIX/BetterDesk/issues/385", + "title": "/opt/rustdesk/.admin_credentials credentials do not work on Docker version of BetterDesk (docker-compose.single.yml and docker-compose.quick.yml builds)", + "repo": "UNITRONIX/BetterDesk", + "created": "2026-08-24T12:30:11Z", + "state": "open", + "comments": 18, + "verified": true + }, + { + "url": "https://github.com/PascaleBeier/hitkeep/issues/353", + "title": "[Bug]: s3 backup status failing; duckdb httpfs extension loading", + "repo": "PascaleBeier/hitkeep", + "created": "2026-09-21T18:45:46Z", + "state": "open", + "comments": 5, + "verified": true + } + ], + "precision": 0.9666666666666667, + "adjusted": 17793 +} \ No newline at end of file diff --git a/bench/data/oci-runtime-exec-failed.json b/bench/data/oci-runtime-exec-failed.json new file mode 100644 index 0000000..1b1d7ae --- /dev/null +++ b/bench/data/oci-runtime-exec-failed.json @@ -0,0 +1,280 @@ +{ + "category": "OCI runtime exec failed", + "rule": "", + "query": "\"OCI runtime exec failed\" is:issue created:>=2021-01-01", + "total": 3479, + "sample": [ + { + "url": "https://github.com/mdubourg001/glci/issues/14", + "title": "OCI runtime exec failed", + "repo": "mdubourg001/glci", + "created": "2021-02-24T21:58:28Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/lockc-project/lockc/issues/184", + "title": "OCI runtime exec failed", + "repo": "lockc-project/lockc", + "created": "2022-02-24T14:17:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/shimataro/ssh-key-action/issues/196", + "title": "OCI runtime exec failed", + "repo": "shimataro/ssh-key-action", + "created": "2021-11-30T13:40:48Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Finsys/dockhand/issues/1519", + "title": "[BUG] Exec/shell fails with exec: \"/app/dockhand\": on direct-connections", + "repo": "Finsys/dockhand", + "created": "2026-09-04T01:35:49Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Twinki14/palworld-server-chart/issues/38", + "title": "Enabling TCP health check breaks daily reboot cronjob", + "repo": "Twinki14/palworld-server-chart", + "created": "2026-08-20T23:32:55Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/opf/openproject-docker-compose/issues/150", + "title": "Healthcheck for web container fails!", + "repo": "opf/openproject-docker-compose", + "created": "2026-03-11T14:40:21Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/NinjaMonkeyGames/palette-controller-professional/issues/3", + "title": "Unit Tests on CI is Broken", + "repo": "NinjaMonkeyGames/palette-controller-professional", + "created": "2026-08-27T00:39:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Datura-ai/lium-io/issues/190", + "title": "Adding an Executor", + "repo": "Datura-ai/lium-io", + "created": "2025-01-15T06:31:14Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/ResearchDataCom/actions/issues/9", + "title": "Linting fails when run via Act due to missing commands", + "repo": "ResearchDataCom/actions", + "created": "2026-07-29T22:26:57Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/CLOUDIS-log-analysis/lets-reproduce-logs/issues/2", + "title": "\uc7ac\ud604 \uae30\ub85d", + "repo": "CLOUDIS-log-analysis/lets-reproduce-logs", + "created": "2026-08-15T09:27:31Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/tbmobb813/WireAssist/issues/180", + "title": "DEPLOYMENT.md's debug command doesn't work \u2014 wget isn't in the runtime image", + "repo": "tbmobb813/WireAssist", + "created": "2026-08-24T08:17:36Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Minsolimbu/dokploy/issues/7", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "Minsolimbu/dokploy", + "created": "2025-10-05T20:38:08Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Muhammad7839/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "Muhammad7839/dokploy", + "created": "2025-09-30T02:07:22Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/timothy1ee/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "timothy1ee/dokploy", + "created": "2025-09-30T01:38:09Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/JanteZhou/dokploy/issues/5", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "JanteZhou/dokploy", + "created": "2025-09-30T01:31:27Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/aliasofsyn/dokploy/issues/4", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "aliasofsyn/dokploy", + "created": "2025-09-30T00:45:23Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docksal/docksal/issues/1856", + "title": "fin db create broken with MariaDB 11.4", + "repo": "docksal/docksal", + "created": "2026-07-20T10:16:48Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/williamyang10/dokploy/issues/7", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "williamyang10/dokploy", + "created": "2025-10-12T23:30:11Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/zenocross/dokploy/issues/5", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "zenocross/dokploy", + "created": "2025-12-10T09:07:03Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/davidfmajek/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "davidfmajek/dokploy", + "created": "2025-09-30T03:51:57Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nathanz19/dokploy/issues/1", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "nathanz19/dokploy", + "created": "2025-09-30T03:28:19Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Thenewgirl04/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "Thenewgirl04/dokploy", + "created": "2025-09-30T01:43:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nehaask/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "nehaask/dokploy", + "created": "2025-09-30T01:08:01Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Praise-creator/dokploy/issues/5", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "Praise-creator/dokploy", + "created": "2025-09-30T00:17:33Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ishs-cloud-computing/eks-study/issues/34", + "title": "[PART-2] module-07: distroless \ucee8\ud14c\uc774\ub108\uc5d0\uc11c JWT \ud1a0\ud070 \ud30c\uc77c \uc811\uadfc \ubd88\uac00", + "repo": "ishs-cloud-computing/eks-study", + "created": "2026-05-31T05:58:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/seunshix/dokploy/issues/7", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "seunshix/dokploy", + "created": "2025-10-10T15:32:58Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nwang10/dokploy/issues/7", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "nwang10/dokploy", + "created": "2025-10-11T14:30:01Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ArmaghanAb/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "ArmaghanAb/dokploy", + "created": "2025-09-30T03:45:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/AbdoulayeSedego/dokploy/issues/3", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "AbdoulayeSedego/dokploy", + "created": "2025-09-30T03:44:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/funny1dog/dokploy/issues/4", + "title": "Issue #5 - [BUG] Docker Compose volume mount causes OCI runtime exec failed error", + "repo": "funny1dog/dokploy", + "created": "2025-09-30T00:29:47Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 3479 +} \ No newline at end of file diff --git a/bench/data/platform--exec-format-error.json b/bench/data/platform--exec-format-error.json new file mode 100644 index 0000000..b2ddc91 --- /dev/null +++ b/bench/data/platform--exec-format-error.json @@ -0,0 +1,280 @@ +{ + "category": "platform: exec format error", + "rule": "image.platform", + "query": "\"exec format error\" docker is:issue created:>=2021-01-01", + "total": 5372, + "sample": [ + { + "url": "https://github.com/pashword/pashword/issues/24", + "title": "docker compose install not working", + "repo": "pashword/pashword", + "created": "2026-09-17T05:29:36Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/onecli/onecli/issues/489", + "title": "exec /usr/local/bin/docker-entrypoint.sh: exec format error", + "repo": "onecli/onecli", + "created": "2026-08-11T13:07:20Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/imagegenius/docker-immich/issues/810", + "title": "When pulling >= v3 on unraid I get an error - Exec format error", + "repo": "imagegenius/docker-immich", + "created": "2026-09-03T20:16:57Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/Muatasim-Aswad/job-tracker/issues/26", + "title": "[Bug]: \"exec uvicorn failed: Exec format error\" when attempting to run job-tracker from docker", + "repo": "Muatasim-Aswad/job-tracker", + "created": "2026-09-11T09:54:57Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Turbootzz/Vaultwarden-API/issues/47", + "title": "Multi-arch image ships the amd64 binary for arm64/arm-v7 (exec format error)", + "repo": "Turbootzz/Vaultwarden-API", + "created": "2026-09-08T23:33:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ResearchDataCom/actions/issues/17", + "title": "\"Exec format error\" running pinact via ResearchDataCom/actions/pre-commit on ubuntu-24.04-arm runners", + "repo": "ResearchDataCom/actions", + "created": "2026-09-01T17:36:46Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Wei-Shaw/sub2api/issues/2061", + "title": "docker image bug\uff1adocker-entrypoint.sh: exec format error", + "repo": "Wei-Shaw/sub2api", + "created": "2026-04-28T09:58:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kimschles/k8s-microservices-examples/issues/1", + "title": "Exec format error", + "repo": "kimschles/k8s-microservices-examples", + "created": "2024-12-30T16:51:39Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dmccormack/nut-client/issues/2", + "title": "exec format error", + "repo": "dmccormack/nut-client", + "created": "2023-12-06T19:03:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/cubicecho/mcp-ragdown/issues/2", + "title": "Publish an arm64 (Apple Silicon) Docker image", + "repo": "cubicecho/mcp-ragdown", + "created": "2026-09-13T22:59:45Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/fonoster/fonoster/issues/862", + "title": "apiserver arm64 image is broken: ships amd64 dockerize binary (Exec format error)", + "repo": "fonoster/fonoster", + "created": "2026-07-02T14:06:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RedisTimeSeries/RedisTimeSeries/issues/869", + "title": "Docker image exec format error", + "repo": "RedisTimeSeries/RedisTimeSeries", + "created": "2021-10-26T17:27:52Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/AbdBarho/stable-diffusion-webui-docker/issues/675", + "title": "exec format error", + "repo": "AbdBarho/stable-diffusion-webui-docker", + "created": "2024-03-12T11:57:24Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/mporrato/alpine-h700/issues/1", + "title": "Exec format error", + "repo": "mporrato/alpine-h700", + "created": "2024-07-28T12:57:43Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/mercxry/p110-energy-monitor/issues/17", + "title": "exec format error", + "repo": "mercxry/p110-energy-monitor", + "created": "2023-03-20T07:54:54Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/getchoo-archive/packwiz-serve/issues/2", + "title": "Exec format error", + "repo": "getchoo-archive/packwiz-serve", + "created": "2023-12-07T03:32:17Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/ex0nuss/Remote-Wake-Sleep-On-LAN-Docker/issues/3", + "title": "exec format error", + "repo": "ex0nuss/Remote-Wake-Sleep-On-LAN-Docker", + "created": "2022-03-09T11:23:14Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/headroomlabs-ai/headroom/issues/2700", + "title": "[BUG] Docker arm64 image ships wrong-arch rtk binary \u2014 dashboard shows \"RTK not installed\" (Exec format error)", + "repo": "headroomlabs-ai/headroom", + "created": "2026-08-01T00:53:19Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/ewfian/faiss-node/issues/53", + "title": "Docker image fail : exec format error", + "repo": "ewfian/faiss-node", + "created": "2025-05-21T16:38:18Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/indiefan/home_assistant_nanit/issues/22", + "title": "exec format error", + "repo": "indiefan/home_assistant_nanit", + "created": "2025-01-24T01:28:50Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/rickraven/conntrack-exporter/issues/1", + "title": "Docker build defaults to amd64 binary on arm64 hosts (exec format error)", + "repo": "rickraven/conntrack-exporter", + "created": "2026-03-29T10:41:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/home-assistant/plugin-dns/issues/207", + "title": "amd64-hassio-dns:2026.08.0 image ships with 0-byte /usr/bin/coredns binary \u2014 DNS plugin fails to start with \"Exec format error\"", + "repo": "home-assistant/plugin-dns", + "created": "2026-08-24T19:45:33Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/christhomas/go-networkfs/issues/16", + "title": "Two test-server images are amd64-only, so the containerised suite cannot run on arm64", + "repo": "christhomas/go-networkfs", + "created": "2026-09-18T10:46:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/TheAppgineer/flatpak-flutter/issues/127", + "title": "docker run command isn't working for linux/arm64/v8", + "repo": "TheAppgineer/flatpak-flutter", + "created": "2026-09-20T09:31:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/argoproj-labs/argocd-ephemeral-access/issues/180", + "title": "Publish multi-arch images (linux/arm64)", + "repo": "argoproj-labs/argocd-ephemeral-access", + "created": "2026-09-02T12:48:12Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/monitoror/monitoror/issues/403", + "title": "exec format error", + "repo": "monitoror/monitoror", + "created": "2021-12-04T15:16:08Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/aws/context-ontology-accelerator/issues/51", + "title": "[Docs]: Update docker configuration instructions for cross-architecture builds", + "repo": "aws/context-ontology-accelerator", + "created": "2026-08-07T22:57:48Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/OCSInventory-NG/OCSInventory-Docker-Image/issues/89", + "title": "/docker-entrypoint.sh: exec format error", + "repo": "OCSInventory-NG/OCSInventory-Docker-Image", + "created": "2022-06-24T12:46:20Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/pathwae/proxy/issues/3", + "title": "Exec format error", + "repo": "pathwae/proxy", + "created": "2022-05-24T17:21:44Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/docker/desktop-feedback/issues/528", + "title": "4.82.0: WSL integration broken \u2014 docker-desktop-user-distro published as 0 bytes \u2192 \"Exec format error\"", + "repo": "docker/desktop-feedback", + "created": "2026-07-19T12:16:25Z", + "state": "open", + "comments": 1, + "verified": true + } + ], + "precision": 0.9666666666666667, + "adjusted": 5193 +} \ No newline at end of file diff --git a/bench/data/platform--no-matching-manifest.json b/bench/data/platform--no-matching-manifest.json new file mode 100644 index 0000000..fdab202 --- /dev/null +++ b/bench/data/platform--no-matching-manifest.json @@ -0,0 +1,271 @@ +{ + "category": "platform: no matching manifest", + "rule": "image.platform", + "query": "\"no matching manifest for\" is:issue created:>=2021-01-01", + "total": 2760, + "sample": [ + { + "url": "https://github.com/ggml-org/whisper.cpp/issues/2081", + "title": "Docker \"no matching manifest for `linux/arm64`\"", + "repo": "ggml-org/whisper.cpp", + "created": "2024-04-20T08:19:44Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/nextcloud/docker/issues/2601", + "title": "35-apache: Pulling from library/nextcloud - no matching manifest for linux/amd64 in the manifest list entries", + "repo": "nextcloud/docker", + "created": "2026-09-17T05:35:40Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/ray-manaloto/dotfiles/issues/985", + "title": "`:dev`'s amd64 half is unpullable on a GHA runner: `no matching manifest for linux/amd64`", + "repo": "ray-manaloto/dotfiles", + "created": "2026-09-09T01:55:15Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/debridmediamanager/debrid-media-manager/issues/146", + "title": "ERROR: no matching manifest for linux/amd64 in the manifest list entries", + "repo": "debridmediamanager/debrid-media-manager", + "created": "2024-12-07T00:08:46Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/NginxProxyManager/nginx-proxy-manager/issues/5379", + "title": "docker pull - no matching manifest for linux/arm/v8", + "repo": "NginxProxyManager/nginx-proxy-manager", + "created": "2026-03-06T06:14:16Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/Smeagolworms4/omv2mqtt/issues/4", + "title": "no matching manifest for linux/386", + "repo": "Smeagolworms4/omv2mqtt", + "created": "2026-03-17T15:25:17Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/lando/pantheon/issues/296", + "title": "no matching manifest for linux/arm64/v8", + "repo": "lando/pantheon", + "created": "2025-11-13T21:54:48Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/calcom/docker/issues/208", + "title": "no matching manifest for linux/arm64/v8", + "repo": "calcom/docker", + "created": "2023-01-30T06:21:20Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/GreenFrogSB/LMDS/issues/76", + "title": "no matching manifest for linux/arm/v8", + "repo": "GreenFrogSB/LMDS", + "created": "2023-10-21T18:58:19Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/helingfeng/Docker-LNMP/issues/10", + "title": "no matching manifest for linux/arm64/v8", + "repo": "helingfeng/Docker-LNMP", + "created": "2022-09-09T12:52:19Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/pglombardo/PasswordPusher/issues/4095", + "title": "Unable to update: \"no matching manifest for linux/arm64/v8\"", + "repo": "pglombardo/PasswordPusher", + "created": "2026-01-09T07:57:49Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/hassancs91/PyRunner/issues/3", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries error", + "repo": "hassancs91/PyRunner", + "created": "2026-05-23T02:13:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/coolguy1842/SaveSyncd/issues/13", + "title": "Mac arm64 compatibility (no matching manifest for linux/arm64/v8)", + "repo": "coolguy1842/SaveSyncd", + "created": "2026-02-18T19:57:39Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/papermerge/papermerge-core/issues/590", + "title": "Unable to pull papermerge/papermerge:3.4 no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "papermerge/papermerge-core", + "created": "2025-03-06T03:19:24Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/OpenCSGs/csghub/issues/661", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "OpenCSGs/csghub", + "created": "2024-09-23T07:51:19Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/SecuritySilverbacks/SAPMAP/issues/47", + "title": "[Documentation] pulling the prebuilt Docker image on macOS (Silicon)", + "repo": "SecuritySilverbacks/SAPMAP", + "created": "2026-09-17T08:20:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/naroat/tools-web/issues/16", + "title": "\u65e0\u6cd5\u62c9\u53d6Docker compose", + "repo": "naroat/tools-web", + "created": "2026-09-16T03:04:59Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/linkwarden/linkwarden/issues/273", + "title": "no matching manifest for linux/arm/v8 in the manifest list entries", + "repo": "linkwarden/linkwarden", + "created": "2023-11-03T15:58:04Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/alpinelinux/docker-alpine/issues/426", + "title": "no matching manifest for linux/amd64 in the manifest list entries", + "repo": "alpinelinux/docker-alpine", + "created": "2024-11-18T17:59:44Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/automation-avenue/youtube-39-arr-apps-1-click/issues/5", + "title": "no matching manifest for linux/amd64 in the manifest list entries", + "repo": "automation-avenue/youtube-39-arr-apps-1-click", + "created": "2025-07-23T03:23:05Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/docker/docs/issues/26086", + "title": "tutorial example image missing for Pi32", + "repo": "docker/docs", + "created": "2026-09-13T04:33:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/multiarch/qemu-user-static/issues/215", + "title": "docker: no matching manifest for linux/amd64 in the manifest list entries", + "repo": "multiarch/qemu-user-static", + "created": "2025-10-22T23:37:26Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Unstructured-IO/unstructured/issues/4028", + "title": "bug/no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "Unstructured-IO/unstructured", + "created": "2025-06-19T13:45:28Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/huntingmill/image-nfs/issues/2", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "huntingmill/image-nfs", + "created": "2024-12-05T03:43:03Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/bryangerlach/rdgen/issues/121", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "bryangerlach/rdgen", + "created": "2025-08-21T14:24:36Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/nicolaka/netshoot/issues/119", + "title": "no matching manifest for linux/arm/v7 in the manifest list entries", + "repo": "nicolaka/netshoot", + "created": "2022-11-08T14:43:33Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/ReverseEngineeringDE/iSmart-Gateway-Home-Assistant-Add-on/issues/15", + "title": "no matching manifest for linux/arm/v7 in the manifest list entries", + "repo": "ReverseEngineeringDE/iSmart-Gateway-Home-Assistant-Add-on", + "created": "2023-09-09T11:56:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/apache/rocketmq-docker/issues/92", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "apache/rocketmq-docker", + "created": "2023-09-21T02:32:49Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/VKSRC/Github-Monitor/issues/131", + "title": "no matching manifest for linux/arm64/v8 in the manifest list entries", + "repo": "VKSRC/Github-Monitor", + "created": "2022-12-01T10:34:51Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 2760 +} \ No newline at end of file diff --git a/bench/data/platform--requested-platform-mismatch.json b/bench/data/platform--requested-platform-mismatch.json new file mode 100644 index 0000000..35cd6b2 --- /dev/null +++ b/bench/data/platform--requested-platform-mismatch.json @@ -0,0 +1,280 @@ +{ + "category": "platform: requested platform mismatch", + "rule": "image.platform", + "query": "\"does not match the detected host platform\" is:issue created:>=2021-01-01", + "total": 3717, + "sample": [ + { + "url": "https://github.com/Kanka-CE/kanka-ce-deploy/issues/8", + "title": "ARM image", + "repo": "Kanka-CE/kanka-ce-deploy", + "created": "2026-09-07T10:57:03Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/SteveLTN/https-portal/issues/326", + "title": "does not match the detected host platform (linux/s390x)", + "repo": "SteveLTN/https-portal", + "created": "2022-11-08T01:26:17Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/super-productivity/plainspace/issues/37", + "title": "ARM64 Support - Docker Compose", + "repo": "super-productivity/plainspace", + "created": "2026-08-31T21:37:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/spectrum-finance/ergo-dex-backend/issues/99", + "title": "events-tracker image does not match the detected host platform", + "repo": "spectrum-finance/ergo-dex-backend", + "created": "2024-02-20T03:11:21Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/qusong0627/QuantMind/issues/24", + "title": "Mac MINI \u7684 VMware \u91cc\u9762\u4f7f\u7528 Ubuntu \u5b89\u88c5\u62a5\u51fa", + "repo": "qusong0627/QuantMind", + "created": "2026-09-06T10:36:17Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/maxking/docker-mailman/issues/731", + "title": "linux/arm64 - The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)", + "repo": "maxking/docker-mailman", + "created": "2024-11-10T17:58:05Z", + "state": "open", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/postgis/docker-postgis/issues/371", + "title": "RaspberryPi: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested.", + "repo": "postgis/docker-postgis", + "created": "2023-12-07T21:34:55Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/TomasTomecek/sen/issues/191", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)", + "repo": "TomasTomecek/sen", + "created": "2026-04-08T14:15:04Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/NilsLeo/kcc-cloud/issues/28", + "title": "[Feature]: ARM64 image support", + "repo": "NilsLeo/kcc-cloud", + "created": "2026-09-01T08:30:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/hkyutong/Wechat-Claw/issues/1", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)", + "repo": "hkyutong/Wechat-Claw", + "created": "2026-03-17T02:47:50Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/portainer/helper-reset-password/issues/7", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform", + "repo": "portainer/helper-reset-password", + "created": "2022-09-09T22:12:36Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/TheAppgineer/flatpak-flutter/issues/127", + "title": "docker run command isn't working for linux/arm64/v8", + "repo": "TheAppgineer/flatpak-flutter", + "created": "2026-09-20T09:31:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/logicallyincorrect/onvif2mqtt/issues/57", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform", + "repo": "logicallyincorrect/onvif2mqtt", + "created": "2022-06-20T13:40:43Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/moby/buildkit/issues/7110", + "title": "BuildKit fails to start container process under Rosetta emulation", + "repo": "moby/buildkit", + "created": "2026-09-02T18:34:23Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/jaegertracing/jaeger-idl/issues/117", + "title": "[Bug]: make protocompile command giving error on apple silicon(arm64)", + "repo": "jaegertracing/jaeger-idl", + "created": "2025-01-15T14:12:20Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/rancher-sandbox/rancher-desktop/issues/5577", + "title": "platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)", + "repo": "rancher-sandbox/rancher-desktop", + "created": "2023-09-23T09:35:14Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/lmnr-ai/lmnr/issues/1299", + "title": "Doesn't work on Mac Apple Silicon", + "repo": "lmnr-ai/lmnr", + "created": "2026-03-02T07:34:54Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/hei85/vohive-qdc507-nas-docker/issues/1", + "title": "\u5927\u4f6c\uff0carm64\u7684\u955c\u50cf\u8981\u600e\u4e48\u505a\u554a\uff1f", + "repo": "hei85/vohive-qdc507-nas-docker", + "created": "2026-08-02T11:22:59Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/TyrantUT/Qt_Docker/issues/1", + "title": " (linux/arm/v7) does not match the detected host platform (linux/amd64/v3)", + "repo": "TyrantUT/Qt_Docker", + "created": "2023-04-18T13:31:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/aahnik/tgcf/issues/550", + "title": "WARNING: The requested image's platform (linux/amd64) does not match the detected host platform", + "repo": "aahnik/tgcf", + "created": "2023-12-25T13:17:39Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/silenceboychen/silenceboychen.github.io/issues/50", + "title": "docker\u8de8\u5e73\u53f0\u6253\u5305\u95ee\u9898does not match the detected host platform | silenceboychen", + "repo": "silenceboychen/silenceboychen.github.io", + "created": "2022-06-28T03:19:35Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/google/grr/issues/1133", + "title": "ARM missing and hung docker compose deployment", + "repo": "google/grr", + "created": "2025-04-22T16:47:30Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/timwuthenow/ibamoe-docker-compose/issues/1", + "title": "The image's platform (linux/arm64) does not match the detected host platform (linux/amd64/v3)", + "repo": "timwuthenow/ibamoe-docker-compose", + "created": "2023-06-29T12:44:31Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/illinoisdata/DeepOLA/issues/180", + "title": "The requested image's platform (linux/arm64/v8) does not match the detected host platform", + "repo": "illinoisdata/DeepOLA", + "created": "2023-08-01T17:57:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tonistiigi/binfmt/issues/115", + "title": "image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) ", + "repo": "tonistiigi/binfmt", + "created": "2022-12-01T06:47:12Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/iboates/osm-utilities-docker/issues/80", + "title": "Multi-arch support: please publish linux/arm64 images", + "repo": "iboates/osm-utilities-docker", + "created": "2026-08-10T10:55:24Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/mincong-h/finance-toolkit/issues/46", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)", + "repo": "mincong-h/finance-toolkit", + "created": "2022-03-20T16:51:27Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/sdelrio/rpi-torrent-box/issues/25", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm/v7)", + "repo": "sdelrio/rpi-torrent-box", + "created": "2021-11-18T18:44:44Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/solana-foundation/solana-verifiable-build/issues/312", + "title": "Solana verified", + "repo": "solana-foundation/solana-verifiable-build", + "created": "2026-05-28T18:19:38Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/Red5d/docker-autocompose/issues/27", + "title": "The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm/v7)", + "repo": "Red5d/docker-autocompose", + "created": "2021-10-03T08:53:10Z", + "state": "open", + "comments": 1, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 3717 +} \ No newline at end of file diff --git a/bench/data/port-already-allocated.json b/bench/data/port-already-allocated.json new file mode 100644 index 0000000..50798f4 --- /dev/null +++ b/bench/data/port-already-allocated.json @@ -0,0 +1,280 @@ +{ + "category": "port already allocated", + "rule": "ports.conflict,ports.host", + "query": "\"port is already allocated\" is:issue created:>=2021-01-01", + "total": 1239, + "sample": [ + { + "url": "https://github.com/cilium/cilium/issues/48950", + "title": "CI: ci-conformance-race: K8sAgentPolicyTest: provided port is already allocated", + "repo": "cilium/cilium", + "created": "2026-09-22T18:10:05Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/cilium/cilium/issues/45792", + "title": "CI: `Invalid value: 31414: provided port is already allocated`", + "repo": "cilium/cilium", + "created": "2026-05-05T15:01:46Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/open-crs/automatic_exploit_generation/issues/7", + "title": "`exploit` command fails with `port is already allocated`", + "repo": "open-crs/automatic_exploit_generation", + "created": "2026-07-05T22:33:40Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/abbgrade/PsSqlTestServer/issues/13", + "title": "Port is already allocated", + "repo": "abbgrade/PsSqlTestServer", + "created": "2022-04-03T13:32:15Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kactlabs/error-archive/issues/306", + "title": "Port is already allocated", + "repo": "kactlabs/error-archive", + "created": "2022-09-23T09:09:46Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/hkust-nlp/Toolathlon/issues/45", + "title": "poste & woocommerce containers fail again (with \"port is already allocated\")", + "repo": "hkust-nlp/Toolathlon", + "created": "2026-05-29T04:28:42Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/SociallyIneptWeeb/LanguageLeapAI/issues/91", + "title": "Error port is already allocated", + "repo": "SociallyIneptWeeb/LanguageLeapAI", + "created": "2023-03-29T15:30:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/docker/for-mac/issues/7130", + "title": "Port is already allocated when trying to start a container", + "repo": "docker/for-mac", + "created": "2024-01-10T20:44:45Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/Liarea/lazyslice/issues/70", + "title": "T-0217: internal/discover/provision: drop Force from the failed-attempt container removal, fix the retry count's off-by-one, and keep the daemon's port error when the range is exhausted", + "repo": "Liarea/lazyslice", + "created": "2026-09-16T19:45:30Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/smartcontractkit/chainlink-node-compose/issues/13", + "title": "Bind for 0.0.0.0:6711 failed: port is already allocated", + "repo": "smartcontractkit/chainlink-node-compose", + "created": "2022-09-29T17:49:40Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/karmada-io/karmada/issues/7823", + "title": "Karmada API server unexpectedly assigned nodePort for service", + "repo": "karmada-io/karmada", + "created": "2026-08-12T09:54:39Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/coleam00/ai-agents-masterclass/issues/7", + "title": "Bind for 0.0.0.0:5432 failed: port is already allocated", + "repo": "coleam00/ai-agents-masterclass", + "created": "2024-10-24T06:07:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/yohan-work/minidog/issues/25", + "title": "Make the Collector's OTLP ports configurable in deploy/compose.yaml", + "repo": "yohan-work/minidog", + "created": "2026-09-15T07:01:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/balena-os/balena-supervisor/issues/1634", + "title": "Device gets stuck in download loop with error `port is already allocated`", + "repo": "balena-os/balena-supervisor", + "created": "2021-03-29T13:44:19Z", + "state": "open", + "comments": 7, + "verified": true + }, + { + "url": "https://github.com/ReddyNikhila/payment-gateway/issues/1", + "title": "[Peer Review] [Setup Blocker] Docker compose fails to start due to port conflicts and corrupted README instructions", + "repo": "ReddyNikhila/payment-gateway", + "created": "2026-07-18T06:48:18Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/open-source-labs/Docketeer/issues/178", + "title": "Docketeer Docker Extension: Bind for 0.0.0.0:5432 failed: port is already allocated", + "repo": "open-source-labs/Docketeer", + "created": "2025-01-24T03:54:51Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/fabric8io/docker-maven-plugin/issues/1874", + "title": "Using docker-maven-plugin from two profiles with different ports causes 'port is already allocated' error", + "repo": "fabric8io/docker-maven-plugin", + "created": "2025-06-23T12:51:44Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ndelangen/dunezone/issues/1055", + "title": "The e2e log stops announcing an expected port collision and two expected curl misses", + "repo": "ndelangen/dunezone", + "created": "2026-09-06T08:59:58Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/trydirect/stacker/issues/241", + "title": "Observability: surface the real failure cause instead of generic \"local-exec provisioner error\" / \"paused due to internal error\"", + "repo": "trydirect/stacker", + "created": "2026-08-22T14:45:18Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/jentic/jentic-one/issues/976", + "title": "[bug] jenticctl update: a host port already in use (5432) fails the stack half after the CLI is swapped, leaving cli/stack diverged \u2014 and the re-run says \"Up to date\"", + "repo": "jentic/jentic-one", + "created": "2026-08-06T11:11:51Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/kazurayam/VisualInspectionInKatalonStudio_Reborn/issues/14", + "title": "How to restort work after \"Bind for 0.0.0.0:3080 failed: port is already allocated.\"", + "repo": "kazurayam/VisualInspectionInKatalonStudio_Reborn", + "created": "2022-02-21T01:24:13Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/YannicHock/claude-mail-mcp/issues/57", + "title": "The integration fixture cannot run in two checkouts at once", + "repo": "YannicHock/claude-mail-mcp", + "created": "2026-09-09T14:30:33Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/sahithi9755/product-catalog-redis-api/issues/1", + "title": "[Peer Review] setup-blocker: API container fails because port 8080 is already allocated", + "repo": "sahithi9755/product-catalog-redis-api", + "created": "2026-06-30T04:37:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kljensen/yelukerest/issues/382", + "title": "REST test stack: pick free ports instead of failing on 55432/8080/8443", + "repo": "kljensen/yelukerest", + "created": "2026-09-10T11:21:31Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/velero-io/velero/issues/3419", + "title": "Restoring a LoadBalancer type service gives failed to allocate requested HealthCheck NodePort: provided port is already allocated error", + "repo": "velero-io/velero", + "created": "2021-02-08T16:26:10Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/nnn-training/faq/issues/9", + "title": "Docker \u306e port is already allocated \u30a8\u30e9\u30fc\u306b\u5bfe\u3059\u308b\u5bfe\u5fdc\u3092\u8ffd\u52a0\u3057\u305f\u3044", + "repo": "nnn-training/faq", + "created": "2021-12-22T04:15:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/watch-wolf/WatchWolf/issues/40", + "title": "Starting WatchWolf fails, yet reports ok", + "repo": "watch-wolf/WatchWolf", + "created": "2026-02-28T13:43:09Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/kosson/koha-alpine/issues/2", + "title": "problem building and starting Koha stack following production Mode B", + "repo": "kosson/koha-alpine", + "created": "2026-08-17T19:27:19Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/opensandbox-group/OpenSandbox/issues/1705", + "title": "Port allocation race: concurrent sandbox creation fails with \"port is already allocated\"", + "repo": "opensandbox-group/OpenSandbox", + "created": "2026-09-02T16:25:33Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/yaso09/tengiz/issues/1983", + "title": "MEDIUM: old port is freed even when the old container removal fails \u2014 freed-but-bound port breaks the next deploy", + "repo": "yaso09/tengiz", + "created": "2026-08-10T09:33:34Z", + "state": "open", + "comments": 0, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 1239 +} \ No newline at end of file diff --git a/bench/data/rootless-privileged-port.json b/bench/data/rootless-privileged-port.json new file mode 100644 index 0000000..564fa1a --- /dev/null +++ b/bench/data/rootless-privileged-port.json @@ -0,0 +1,280 @@ +{ + "category": "rootless privileged port", + "rule": "ports.host", + "query": "\"cannot expose privileged port\" is:issue created:>=2021-01-01", + "total": 39, + "sample": [ + { + "url": "https://github.com/faramirezs/voice-chef/issues/227", + "title": "[Integration/Network] cannot expose privileged port 80", + "repo": "faramirezs/voice-chef", + "created": "2026-05-07T13:43:27Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/gutmensch/docker-dmarc-report/issues/66", + "title": "rootlessport cannot expose privileged port 80", + "repo": "gutmensch/docker-dmarc-report", + "created": "2025-01-13T00:10:56Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/abdo2048/Ask-Brave-Chat-Exporter/issues/3", + "title": "Export error", + "repo": "abdo2048/Ask-Brave-Chat-Exporter", + "created": "2026-08-19T06:51:54Z", + "state": "open", + "comments": 0, + "verified": false + }, + { + "url": "https://github.com/docker/awesome-compose/issues/324", + "title": "wordpress-mysql - port mapping issue", + "repo": "docker/awesome-compose", + "created": "2023-02-06T07:49:49Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/ublue-os/forge/issues/28", + "title": "Prerequisites check on forge.sh heat-up", + "repo": "ublue-os/forge", + "created": "2024-05-02T06:16:37Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/yuzu-juice/ft_transcendence/issues/129", + "title": "[bug] failed to set up container networking", + "repo": "yuzu-juice/ft_transcendence", + "created": "2026-09-10T08:38:42Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/tim-daria/poolaki/issues/78", + "title": "Clarify Docker rootless mode and privileged ports (80/443)", + "repo": "tim-daria/poolaki", + "created": "2026-07-23T05:40:01Z", + "state": "closed", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/cvoya-com/spring-voyage/issues/2926", + "title": "dispatcher-smoke-full.yml: prep unprivileged-port floor before `deploy.sh up`", + "repo": "cvoya-com/spring-voyage", + "created": "2026-05-30T01:41:37Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/devobagmbh/talos-platform-apps/issues/168", + "title": "docs(AGENTS+build-CONVENTIONS): reconcile stale \"no live cluster\" claim with the local Talos cluster", + "repo": "devobagmbh/talos-platform-apps", + "created": "2026-06-14T13:46:04Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Blackhole-Megalaser/transcendence/issues/49", + "title": "chat seems broken, cannot connect to redis", + "repo": "Blackhole-Megalaser/transcendence", + "created": "2026-05-08T13:02:44Z", + "state": "closed", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/LibreBooking/docker/issues/139", + "title": "podman: rootlessport cannot expose privileged port 80", + "repo": "LibreBooking/docker", + "created": "2026-01-23T08:59:48Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/cvoya-com/spring-voyage/issues/2924", + "title": "install.sh: rootless Podman can't bind 80/443 \u2014 pre-flight checks availability, not bindability", + "repo": "cvoya-com/spring-voyage", + "created": "2026-05-30T01:23:06Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/knative-extensions/kn-plugin-quickstart/issues/542", + "title": "Using port 80 causes a fail on kind with rootless podman", + "repo": "knative-extensions/kn-plugin-quickstart", + "created": "2024-10-08T13:42:13Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/bbvch-ai/aihub-core/issues/1364", + "title": "web image healthcheck targets port 80 but nginx listens on 8080", + "repo": "bbvch-ai/aihub-core", + "created": "2026-05-27T09:58:51Z", + "state": "closed", + "comments": 2, + "verified": false + }, + { + "url": "https://github.com/ddev/ddev/issues/8382", + "title": "\"linux\" missing from OSTYPE options", + "repo": "ddev/ddev", + "created": "2026-05-03T13:57:06Z", + "state": "closed", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/podman-desktop/podman-desktop/issues/1219", + "title": "Warn user when the pod cannot be started ", + "repo": "podman-desktop/podman-desktop", + "created": "2023-01-23T09:45:40Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/podman-desktop/podman-desktop/issues/2305", + "title": "Provide an option to configure the podman-machine to be rootless or rootful", + "repo": "podman-desktop/podman-desktop", + "created": "2023-05-02T14:31:39Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/markshust/docker-magento/issues/1370", + "title": "Neither the automated setup nor the manual setup are able to install docker-magento in Ubuntu with Docker Rootless mode", + "repo": "markshust/docker-magento", + "created": "2025-08-23T17:08:28Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/mikesvoboda/nemotron-v3-home-security-intelligence/issues/3050", + "title": "Fix FRONTEND_HTTPS_PORT default causing privileged port bind error", + "repo": "mikesvoboda/nemotron-v3-home-security-intelligence", + "created": "2026-01-14T21:28:44Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/CanastaWiki/Canasta-CLI/issues/555", + "title": "create fails", + "repo": "CanastaWiki/Canasta-CLI", + "created": "2026-05-11T00:04:24Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/appwrite/appwrite/issues/2463", + "title": "\ud83d\ude80 Feature: running in Podman - a writeup how to go about it", + "repo": "appwrite/appwrite", + "created": "2021-12-06T10:14:50Z", + "state": "closed", + "comments": 11, + "verified": true + }, + { + "url": "https://github.com/ft-trans/ft-transcendence/issues/147", + "title": "\u6821\u820e\u74b0\u5883\u3067\u306f\u6a29\u9650\u4e0d\u8db3\u306e\u305f\u30811024\u4ee5\u4e0b\u306e\u30dd\u30fc\u30c8\u304c\u4f7f\u7528\u3067\u304d\u306a\u3044", + "repo": "ft-trans/ft-transcendence", + "created": "2025-10-18T03:26:41Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/yyamasak02/ft_transcendence/issues/35", + "title": "\u3010\u30d0\u30b0\u3011\u6821\u820e\u74b0\u5883\u3067\u30d3\u30eb\u30c9\uff06\u5b9f\u884c\u3067\u304d\u306a\u3044", + "repo": "yyamasak02/ft_transcendence", + "created": "2025-11-06T06:11:52Z", + "state": "closed", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/DRYTRIX/TimeTracker/issues/104", + "title": "Recommended Install not working", + "repo": "DRYTRIX/TimeTracker", + "created": "2025-10-21T09:58:30Z", + "state": "closed", + "comments": 13, + "verified": true + }, + { + "url": "https://github.com/Blue-Dots-Economy/signals-dpg/issues/539", + "title": "[Sec-Audit][A4] Migrate container base images to Docker Hardened Images (all repos)", + "repo": "Blue-Dots-Economy/signals-dpg", + "created": "2026-08-12T11:49:52Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/onyx-dot-app/onyx/issues/5560", + "title": "Change port from 80 for rootless podman", + "repo": "onyx-dot-app/onyx", + "created": "2025-10-01T10:32:15Z", + "state": "closed", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/Blue-Dots-Economy/bluedots-automation/issues/136", + "title": "Migrate all org container images to Docker Hardened Images (DHI)", + "repo": "Blue-Dots-Economy/bluedots-automation", + "created": "2026-08-12T11:56:58Z", + "state": "closed", + "comments": 1, + "verified": false + }, + { + "url": "https://github.com/fosrl/pangolin/issues/1428", + "title": "Unable to start traefik container due to \"Error: statfs /home/ubuntu/config/traefik/logs: no such file or directory\"", + "repo": "fosrl/pangolin", + "created": "2025-09-07T14:54:54Z", + "state": "closed", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/masaruo/transcendence/issues/53", + "title": "\u672c\u756a\u74b0\u5883\uff1a\u6821\u820e\u306ePC\u3067\u5b9f\u884c", + "repo": "masaruo/transcendence", + "created": "2025-05-27T11:25:28Z", + "state": "closed", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Scrybbling-together/scrybble-site/issues/5", + "title": "Change port", + "repo": "Scrybbling-together/scrybble-site", + "created": "2024-01-13T13:06:27Z", + "state": "closed", + "comments": 8, + "verified": true + } + ], + "precision": 0.8666666666666667, + "adjusted": 34 +} \ No newline at end of file diff --git a/bench/data/volume-permission-denied.json b/bench/data/volume-permission-denied.json new file mode 100644 index 0000000..f4224b2 --- /dev/null +++ b/bench/data/volume-permission-denied.json @@ -0,0 +1,280 @@ +{ + "category": "volume permission denied", + "rule": "", + "query": "docker volume \"permission denied\" container is:issue created:>=2021-01-01", + "total": 6073, + "sample": [ + { + "url": "https://github.com/Dodoooh/brother_ql_app/issues/50", + "title": "permission denied on container start", + "repo": "Dodoooh/brother_ql_app", + "created": "2026-09-11T17:09:35Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/kavix/why/issues/8", + "title": "[Adapter] Docker Diagnostic Adapter: why docker compose up", + "repo": "kavix/why", + "created": "2026-09-20T03:39:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dvd-dev/hilo/issues/1028", + "title": "[Bug] [Errno 13] Permission denied on /run/s6-rc in Docker / Unraid environments", + "repo": "dvd-dev/hilo", + "created": "2026-09-03T02:00:20Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/AppDaemon/appdaemon/issues/2642", + "title": "requirements.txt not installed on container startup: permission denied", + "repo": "AppDaemon/appdaemon", + "created": "2026-06-16T09:43:25Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Part-DB/Part-DB-server/issues/1412", + "title": "[Docker] Permission denied on manual backups when mounting persistent backup volume", + "repo": "Part-DB/Part-DB-server", + "created": "2026-06-15T08:04:52Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/headroomlabs-ai/headroom/issues/3534", + "title": "Docker wrapper: wrap opencode broken - missing .config mount, unbound variable, empty OPENCODE_CONFIG_CONTENT", + "repo": "headroomlabs-ai/headroom", + "created": "2026-09-10T17:27:00Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/microsoft/mssql-docker/issues/940", + "title": "SQL Server 2025-latest container fails to start", + "repo": "microsoft/mssql-docker", + "created": "2025-08-07T18:22:55Z", + "state": "open", + "comments": 17, + "verified": true + }, + { + "url": "https://github.com/algorandfoundation/intermezzo/issues/34", + "title": "EACCES Permission Denied on `/opt/app/node_modules`", + "repo": "algorandfoundation/intermezzo", + "created": "2026-06-02T11:16:25Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/officialjaware/focalboard-reborn/issues/36", + "title": "[issue#5011] permission denied", + "repo": "officialjaware/focalboard-reborn", + "created": "2026-04-08T17:20:41Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/mathieu-keller/anki-sync-server/issues/78", + "title": "Permission denied when creating SYNC_BASE inside Docker container", + "repo": "mathieu-keller/anki-sync-server", + "created": "2026-05-25T22:26:30Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/dave0875/fitness-pals.com/issues/170", + "title": "Prevent production deploys from colliding with legacy Compose containers", + "repo": "dave0875/fitness-pals.com", + "created": "2026-09-08T12:26:59Z", + "state": "open", + "comments": 5, + "verified": true + }, + { + "url": "https://github.com/Dispatcharr/Dispatcharr/issues/1742", + "title": "[Bug]: Installed plugins become unreadable (Errno 13) after container recreate on SELinux hosts \u2014 installer copies SELinux labels via cross-filesystem shutil.move", + "repo": "Dispatcharr/Dispatcharr", + "created": "2026-09-23T18:19:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/taso-labs/freeciv-llm/issues/7", + "title": "Fresh Docker Compose setup cannot write service logs", + "repo": "taso-labs/freeciv-llm", + "created": "2026-09-09T19:54:31Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/testcontainers/testcontainers-go/issues/3157", + "title": "[Bug]: Permission denied reading redpanda.yaml stopping container from proper start", + "repo": "testcontainers/testcontainers-go", + "created": "2025-05-07T09:07:22Z", + "state": "open", + "comments": 10, + "verified": true + }, + { + "url": "https://github.com/zarf-dev/zarf/issues/4669", + "title": "\"filesystem: mkdir /var/lib/registry/docker: permission denied\" during zarf init", + "repo": "zarf-dev/zarf", + "created": "2026-03-03T09:35:28Z", + "state": "open", + "comments": 8, + "verified": true + }, + { + "url": "https://github.com/rmcrackan/Libation/issues/2061", + "title": "Docker duplicate not persisting permissions on Synology NAS", + "repo": "rmcrackan/Libation", + "created": "2026-09-14T03:11:05Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/Finsys/dockhand/issues/1076", + "title": "[BUG] Trivy and Grype scans fail on Hawser with rootless Podman during container update", + "repo": "Finsys/dockhand", + "created": "2026-05-20T19:05:43Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/Dadoum/anisette-v3-server/issues/8", + "title": "Permission denied error", + "repo": "Dadoum/anisette-v3-server", + "created": "2023-11-20T10:19:40Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/mutcherlarahulkumar/leetcode-clone/issues/3", + "title": "Add sandboxed code execution for submissions", + "repo": "mutcherlarahulkumar/leetcode-clone", + "created": "2026-09-09T18:00:45Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/zvchei/docker-seed/issues/29", + "title": "Root-stage mkdirs under $HOME break the user-stage volume mkdirs", + "repo": "zvchei/docker-seed", + "created": "2026-09-02T09:01:56Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/radius-project/ai-extensions/issues/752", + "title": "containers: no way to set fsGroup, so non-root images cannot write to a mounted persistentVolume", + "repo": "radius-project/ai-extensions", + "created": "2026-09-03T21:47:39Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/azerothcore/azerothcore-wotlk/issues/27362", + "title": "Docker, fresh install fails to start due to ac-client-data-init", + "repo": "azerothcore/azerothcore-wotlk", + "created": "2026-08-28T08:28:11Z", + "state": "open", + "comments": 4, + "verified": true + }, + { + "url": "https://github.com/elicify-ai/omnipus/issues/655", + "title": "docker/Dockerfile.heavy: image cannot boot with a Docker-managed volume \u2014 .omnipus is created root-owned", + "repo": "elicify-ai/omnipus", + "created": "2026-08-28T03:47:16Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/RijksICTGilde/hackathon-claude-code/issues/82", + "title": "userns-remap: container-naar-container-netwerken en container-root loskoppelen van host-root", + "repo": "RijksICTGilde/hackathon-claude-code", + "created": "2026-08-03T11:04:40Z", + "state": "open", + "comments": 2, + "verified": true + }, + { + "url": "https://github.com/t0mer/skryol/issues/2", + "title": "Settings save fails with 'permission denied' in the official Docker image (cwd is unwritable)", + "repo": "t0mer/skryol", + "created": "2026-07-12T19:06:38Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/kestra-io/kestra/issues/19626", + "title": "Podman Compose rootless deployment mounts Docker socket instead of the Podman socket", + "repo": "kestra-io/kestra", + "created": "2026-09-19T12:14:08Z", + "state": "open", + "comments": 3, + "verified": true + }, + { + "url": "https://github.com/heuwels/lector/issues/606", + "title": "Linux bind-mount ./data is unusable on first boot (EACCES)", + "repo": "heuwels/lector", + "created": "2026-09-02T23:58:26Z", + "state": "open", + "comments": 0, + "verified": true + }, + { + "url": "https://github.com/joecarl/mularr/issues/35", + "title": "Appdata folder issue", + "repo": "joecarl/mularr", + "created": "2026-09-05T06:10:04Z", + "state": "open", + "comments": 1, + "verified": true + }, + { + "url": "https://github.com/internetarchive/openlibrary/issues/13586", + "title": "Linux Docker Issues: Fresh clone does not boot: web exits 3 writing _testing-prs.json into a bind mount it cannot write", + "repo": "internetarchive/openlibrary", + "created": "2026-09-08T01:35:48Z", + "state": "open", + "comments": 6, + "verified": true + }, + { + "url": "https://github.com/codidact/qpixel/issues/2070", + "title": "`mysqld: File './binlog.index' not found (OS errno 13 - Permission denied)`", + "repo": "codidact/qpixel", + "created": "2026-06-18T14:26:43Z", + "state": "open", + "comments": 3, + "verified": true + } + ], + "precision": 1.0, + "adjusted": 6073 +} \ No newline at end of file diff --git a/bench/mine.py b/bench/mine.py new file mode 100644 index 0000000..5c2f63c --- /dev/null +++ b/bench/mine.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Mine GitHub issues for container deployment failures. + +For each failure category, searches GitHub issues for the error message Docker +prints, records how many issues match (a prevalence proxy) and saves a sample +for manual labelling. Categories preflight does not cover are included on +purpose, so coverage is measured against all common failures, not only the +ones the tool was designed for. + +Requires the `gh` CLI, logged in. Usage: python3 bench/mine.py [--sample N] +""" +import argparse, csv, json, os, subprocess, sys, time, urllib.parse + +# category, preflight rule covering it ("" = not covered), search phrase +CATEGORIES = [ + ("platform: exec format error", "image.platform", '"exec format error" docker'), + ("platform: no matching manifest", "image.platform", '"no matching manifest for"'), + ("platform: requested platform mismatch", "image.platform", '"does not match the detected host platform"'), + ("image: tag/repo not found", "image.exists", '"manifest unknown" docker'), + ("image: pull access denied", "image.exists", '"pull access denied" "may require"'), + ("credentials helper missing", "registry.credentials",'"error getting credentials" "docker-credential"'), + ("build: COPY source not found", "build.context", '"failed to compute cache key" "not found"'), + ("build: COPY failed legacy", "build.context", '"COPY failed: file not found in build context"'), + ("build: outside build context", "build.context", '"forbidden path outside the build context"'), + ("build: BuildKit required", "build.buildkit", '"requires BuildKit"'), + ("compose: required variable", "compose.env", '"required variable" "is missing a value"'), + ("compose: variable not set", "compose.env", '"variable is not set. Defaulting to a blank string"'), + ("compose: unsupported property", "compose.version", '"additional property" "is not allowed" compose'), + ("port already allocated", "ports.conflict,ports.host", '"port is already allocated"'), + ("address already in use", "ports.host", '"address already in use" docker'), + ("rootless privileged port", "ports.host", '"cannot expose privileged port"'), + ("bind: dir onto file", "mounts.bind", '"Are you trying to mount a directory onto a file"'), + ("bind: source does not exist", "mounts.bind", '"bind source path does not exist"'), + ("desktop: mounts denied", "mounts.sharing", '"Mounts denied" "is not shared from the host"'), + ("memory: OOMKilled", "resources.memory", 'OOMKilled "exit code 137"'), + ("memory: mssql minimum", "resources.memory", '"requires a machine with at least 2000 megabytes"'), + ("disk: no space left", "resources.disk,host.disk", '"no space left on device" docker pull'), + ("gpu: no device driver", "resources.gpu", '"could not select device driver" "capabilities: [[gpu]]"'), + ("kernel: vm.max_map_count", "kernel.sysctl", '"vm.max_map_count" "is too low"'), + ("daemon: cannot connect", "(root cause)", '"Cannot connect to the Docker daemon"'), + ("emulation: qemu crash", "image.platform", '"qemu: uncaught target signal"'), + # Not covered by preflight: measured to keep coverage claims honest. + ("volume permission denied", "", 'docker volume "permission denied" container'), + ("DNS resolution in container", "", '"Temporary failure in name resolution" docker'), + ("container healthcheck unhealthy", "", '"is unhealthy" "dependency failed to start"'), + ("network not found", "", 'docker "network" "not found" compose'), + ("OCI runtime exec failed", "", '"OCI runtime exec failed"'), + ("app config/env missing at runtime", "(learned)", '"Database is uninitialized and superuser password is not specified"'), +] + +def gh_api(path): + for attempt in range(5): + r = subprocess.run(["gh", "api", "-H", "Accept: application/vnd.github+json", path], + capture_output=True, text=True) + if r.returncode == 0: + return json.loads(r.stdout) + if "rate limit" in r.stderr.lower() or "403" in r.stderr: + time.sleep(20 * (attempt + 1)) + continue + raise RuntimeError(r.stderr.strip()) + raise RuntimeError("rate limited") + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--sample", type=int, default=30) + ap.add_argument("--since", default="2021-01-01") + ap.add_argument("--out", default=os.path.join(os.path.dirname(__file__), "data")) + args = ap.parse_args() + os.makedirs(args.out, exist_ok=True) + + rows = [] + for cat, rule, phrase in CATEGORIES: + q = f"{phrase} is:issue created:>={args.since}" + res = gh_api("search/issues?per_page=%d&q=%s" % (args.sample, urllib.parse.quote(q))) + items = [{ + "url": i["html_url"], "title": i["title"], + "repo": i["repository_url"].split("/repos/")[1], + "created": i["created_at"], "state": i["state"], "comments": i["comments"], + } for i in res.get("items", [])] + slug = "".join(c if c.isalnum() else "-" for c in cat.lower()).strip("-") + with open(os.path.join(args.out, f"{slug}.json"), "w") as f: + json.dump({"category": cat, "rule": rule, "query": q, + "total": res["total_count"], "sample": items}, f, indent=1) + rows.append((cat, rule, res["total_count"])) + print(f"{res['total_count']:>7} {cat}", file=sys.stderr) + time.sleep(2.5) # search API: 30 requests/minute + + with open(os.path.join(args.out, "counts.csv"), "w", newline="") as f: + w = csv.writer(f) + w.writerow(["category", "preflight_rule", "github_issues_since_" + args.since]) + w.writerows(sorted(rows, key=lambda r: -r[2])) + +if __name__ == "__main__": + main() diff --git a/bench/precision.py b/bench/precision.py new file mode 100644 index 0000000..7563d97 --- /dev/null +++ b/bench/precision.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Estimate how many search hits really contain each category's error. + +GitHub issue search matches words loosely, so raw counts overstate +prevalence. For every sampled issue, this fetches the body and comments and +checks for the category's exact error text. precision = verified / sampled, +and adjusted = total × precision. +""" +import csv, glob, json, os, subprocess, sys + +# Exact text (lowercase) that must appear for an issue to count. +MUST = { + "platform: exec format error": ["exec format error"], + "platform: no matching manifest": ["no matching manifest for"], + "platform: requested platform mismatch": ["does not match the detected host platform"], + "image: tag/repo not found": ["manifest unknown"], + "image: pull access denied": ["pull access denied"], + "credentials helper missing": ["error getting credentials"], + "build: COPY source not found": ["failed to compute cache key"], + "build: COPY failed legacy": ["copy failed: file not found in build context"], + "build: outside build context": ["outside the build context"], + "build: BuildKit required": ["requires buildkit"], + "compose: required variable": ["is missing a value"], + "compose: variable not set": ["variable is not set. defaulting to a blank string"], + "compose: unsupported property": ["additional property", "not allowed"], + "port already allocated": ["port is already allocated"], + "address already in use": ["address already in use"], + "rootless privileged port": ["cannot expose privileged port"], + "bind: dir onto file": ["mount a directory onto a file"], + "bind: source does not exist": ["bind source path does not exist"], + "desktop: mounts denied": ["mounts denied"], + "memory: OOMKilled": ["oomkilled"], + "memory: mssql minimum": ["at least 2000 megabytes"], + "disk: no space left": ["no space left on device"], + "gpu: no device driver": ["could not select device driver"], + "kernel: vm.max_map_count": ["vm.max_map_count", "too low"], + "daemon: cannot connect": ["cannot connect to the docker daemon"], + "emulation: qemu crash": ["qemu: uncaught target signal"], + "volume permission denied": ["permission denied"], + "DNS resolution in container": ["temporary failure in name resolution"], + "container healthcheck unhealthy": ["is unhealthy"], + "network not found": ["network", "not found"], + "OCI runtime exec failed": ["oci runtime exec failed"], + "app config/env missing at runtime": ["superuser password is not specified"], +} + +def text_of(url): + path = url.replace("https://github.com/", "repos/").replace("/issues/", "/issues/", 1) + parts = [] + for p in (path, path + "/comments?per_page=50"): + r = subprocess.run(["gh", "api", p], capture_output=True, text=True) + if r.returncode != 0: + continue + data = json.loads(r.stdout) + items = data if isinstance(data, list) else [data] + for it in items: + parts.append((it.get("title") or "") + "\n" + (it.get("body") or "")) + return "\n".join(parts).lower() + +def main(): + here = os.path.join(os.path.dirname(__file__), "data") + rows = [] + for path in sorted(glob.glob(os.path.join(here, "*.json"))): + d = json.load(open(path)) + must = MUST[d["category"]] + verified = 0 + for item in d["sample"]: + if "verified" not in item: + t = text_of(item["url"]) + item["verified"] = all(m in t for m in must) + verified += item["verified"] + n = len(d["sample"]) + d["precision"] = verified / n if n else 0 + d["adjusted"] = round(d["total"] * d["precision"]) + json.dump(d, open(path, "w"), indent=1) + rows.append((d["category"], d["rule"], d["total"], n, verified, round(d["precision"], 2), d["adjusted"])) + print(f"{d['adjusted']:>7} adj {verified:>2}/{n:<2} {d['category']}", file=sys.stderr) + with open(os.path.join(here, "counts.csv"), "w", newline="") as f: + w = csv.writer(f) + w.writerow(["category", "preflight_rule", "raw_hits", "sampled", "verified", "precision", "adjusted_estimate"]) + w.writerows(sorted(rows, key=lambda r: -r[6])) + +if __name__ == "__main__": + main() From 9491cb45890b0f48cb391e99693c76ba82e43445 Mon Sep 17 00:00:00 2001 From: raphgm Date: Thu, 24 Sep 2026 02:20:04 +0100 Subject: [PATCH 05/23] fix(preflight): time out probe commands and diagnose hung daemons A daemon that is starting or starved of memory accepts connections but never answers, which hung preflight. Probe commands now time out after 20s, and the diagnosis names competing Docker VMs when several run. --- pkg/preflight/host/diagnose.go | 13 +++++++++++++ pkg/preflight/host/diagnose_test.go | 12 ++++++++++++ pkg/preflight/host/host.go | 17 +++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/pkg/preflight/host/diagnose.go b/pkg/preflight/host/diagnose.go index ca33a8a..2d90e26 100644 --- a/pkg/preflight/host/diagnose.go +++ b/pkg/preflight/host/diagnose.go @@ -50,6 +50,19 @@ func diagnoseDaemon(c daemonClues) fact.Failure { } switch { + case strings.Contains(c.Err, "did not answer within"): + f.Summary = "Docker daemon accepts connections but does not answer" + fix := "Wait for it to finish starting; if it stays stuck, restart it." + if len(c.Running) > 1 { + var names []string + for _, r := range c.Running { + names = append(names, r.Name) + } + fix = "Several Docker VMs are running (" + strings.Join(names, ", ") + ") and compete for memory. Stop all but one, e.g. `colima stop`." + } + f.Fix = fix + return f + case strings.Contains(c.Err, "permission denied"): f.Summary = "No permission to use the Docker socket " + socket f.Fix = "Add yourself to the docker group (`sudo usermod -aG docker $USER`, then log in again), or use rootless Docker." diff --git a/pkg/preflight/host/diagnose_test.go b/pkg/preflight/host/diagnose_test.go index 46fc0ff..8097530 100644 --- a/pkg/preflight/host/diagnose_test.go +++ b/pkg/preflight/host/diagnose_test.go @@ -73,3 +73,15 @@ func TestDiagnoseFullDiskBlocksVMStart(t *testing.T) { t.Errorf("fix = %q", f.Fix) } } + +func TestDiagnoseHungDaemonWithTwoVMs(t *testing.T) { + desktop := engine{"Docker Desktop", "/h/.docker/run/docker.sock", "open -a Docker", "desktop-linux"} + f := diagnoseDaemon(daemonClues{ + Endpoint: "unix:///h/.docker/run/docker.sock", + Err: "`docker info --format {{json .}}` did not answer within 20s (the daemon may still be starting, or be out of memory)", + Running: []engine{colima, desktop}, + }) + if f.Summary != "Docker daemon accepts connections but does not answer" || !strings.Contains(f.Fix, "colima stop") { + t.Errorf("got %q / %q", f.Summary, f.Fix) + } +} diff --git a/pkg/preflight/host/host.go b/pkg/preflight/host/host.go index d8474f5..a3f142d 100644 --- a/pkg/preflight/host/host.go +++ b/pkg/preflight/host/host.go @@ -141,6 +141,7 @@ func (p *Profile) PortHolder(ip string, port int, proto string) (holder string, // Probe inspects the host. Anything it cannot determine is recorded in // facts with the reason, so dependent rules report the true root cause. func Probe(ctx context.Context, run executor.Runner, facts *fact.Set) *Profile { + run = timeoutRunner{run} p := &Profile{ ClientOS: runtime.GOOS, ClientArch: runtime.GOARCH, @@ -542,6 +543,22 @@ func missingCredentialHelper() string { return "" } +// commandTimeout bounds each probe command: a daemon that is starting or +// starved of memory accepts connections but never answers. +const commandTimeout = 20 * time.Second + +type timeoutRunner struct{ executor.Runner } + +func (t timeoutRunner) Run(ctx context.Context, name string, args ...string) (string, error) { + cctx, cancel := context.WithTimeout(ctx, commandTimeout) + defer cancel() + out, err := t.Runner.Run(cctx, name, args...) + if err != nil && cctx.Err() == context.DeadlineExceeded { + return out, fmt.Errorf("`%s %s` did not answer within %s (the daemon may still be starting, or be out of memory)", name, strings.Join(args, " "), commandTimeout) + } + return out, err +} + func errText(err error) string { if errors.Is(err, osexec.ErrNotFound) { return "`docker` executable not found on PATH" From 70a977c57f813ac7bb5d76d570812b97f95aad6e Mon Sep 17 00:00:00 2001 From: raphgm Date: Thu, 24 Sep 2026 02:22:54 +0100 Subject: [PATCH 06/23] feat(preflight): predict bind-mount permission failures Reads the container user from Compose or the image config (via the registry) and flags bind mounts a non-root container cannot write: sources Docker will create as root, host directories owned by another uid, and SELinux hosts without :z/:Z. Skipped where Docker Desktop, Colima or OrbStack map ownership. Volume permission errors were the largest uncovered category in the GitHub issue sample. --- README.md | 1 + pkg/preflight/host/host.go | 11 +++ pkg/preflight/project/load.go | 7 +- pkg/preflight/project/project.go | 6 ++ pkg/preflight/registry/registry.go | 7 ++ pkg/preflight/registry/user_live_test.go | 24 +++++ pkg/preflight/rules/owner_unix.go | 17 ++++ pkg/preflight/rules/owner_windows.go | 5 + pkg/preflight/rules/permissions.go | 112 +++++++++++++++++++++++ pkg/preflight/rules/rule.go | 1 + pkg/preflight/rules/rules_test.go | 52 +++++++++++ 11 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 pkg/preflight/registry/user_live_test.go create mode 100644 pkg/preflight/rules/owner_unix.go create mode 100644 pkg/preflight/rules/owner_windows.go create mode 100644 pkg/preflight/rules/permissions.go diff --git a/README.md b/README.md index 64e0f66..ec540ba 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Example from a real run: Postgres started without a password failed. `learn` cre | `ports.conflict` | two services, or several replicas, on one host port | | `ports.host` | host port already held by another process or container; privileged ports under rootless Docker | | `mounts.bind` | missing bind sources that Docker will turn into directories, stale empty directories | +| `mounts.permissions` | non-root containers writing to bind mounts Docker creates as root or that another uid owns; SELinux without `:z` | | `mounts.sharing` | paths outside Docker Desktop's shared folders | Use `--format json` for machine-readable output and `--offline` to skip registry lookups. The exit code is 1 when a blocking problem is predicted. diff --git a/pkg/preflight/host/host.go b/pkg/preflight/host/host.go index a3f142d..3a97cf9 100644 --- a/pkg/preflight/host/host.go +++ b/pkg/preflight/host/host.go @@ -45,6 +45,7 @@ type Profile struct { NCPU int Runtimes []string Rootless bool + SELinux bool CgroupVersion string DockerRootDir string Endpoint string @@ -103,6 +104,13 @@ func (p *Profile) UsesVM() bool { return p.Local && (p.ClientOS == "darwin" || p.ClientOS == "windows" || p.IsDesktop() || strings.Contains(p.Endpoint, "/.colima/")) } +// MapsOwnership reports whether bind-mounted files appear owned by the +// container user regardless of host ownership: true for the file sharing of +// Docker Desktop, Colima and OrbStack on macOS and Windows. +func (p *Profile) MapsOwnership() bool { + return p.ClientOS == "darwin" || p.ClientOS == "windows" +} + // IsDesktop reports whether the daemon runs inside Docker Desktop's VM. func (p *Profile) IsDesktop() bool { return strings.Contains(p.OperatingSystem, "Docker Desktop") @@ -211,6 +219,9 @@ func (p *Profile) probeDaemon(ctx context.Context, run executor.Runner, facts *f if strings.Contains(opt, "name=rootless") { p.Rootless = true } + if strings.Contains(opt, "name=selinux") { + p.SELinux = true + } } if p.IsDesktop() { diff --git a/pkg/preflight/project/load.go b/pkg/preflight/project/load.go index 50fa5a6..b631753 100644 --- a/pkg/preflight/project/load.go +++ b/pkg/preflight/project/load.go @@ -141,6 +141,7 @@ func convertService(sc types.ServiceConfig) *Service { svc := &Service{ Name: sc.Name, Platform: sc.Platform, + User: sc.User, MemLimit: int64(sc.MemLimit), Replicas: 1, Environment: map[string]string{}, @@ -227,11 +228,11 @@ func convertService(sc types.ServiceConfig) *Service { if v.Type != types.VolumeTypeBind { continue } - create := true + create, selinux := true, "" if v.Bind != nil { - create = bool(v.Bind.CreateHostPath) + create, selinux = bool(v.Bind.CreateHostPath), v.Bind.SELinux } - svc.Binds = append(svc.Binds, Bind{Source: v.Source, Target: v.Target, CreateHostPath: create}) + svc.Binds = append(svc.Binds, Bind{Source: v.Source, Target: v.Target, CreateHostPath: create, ReadOnly: v.ReadOnly, SELinux: selinux}) } return svc } diff --git a/pkg/preflight/project/project.go b/pkg/preflight/project/project.go index daf4bb3..4362957 100644 --- a/pkg/preflight/project/project.go +++ b/pkg/preflight/project/project.go @@ -67,6 +67,9 @@ type Service struct { GPU bool + // User is the Compose `user:` value, if any. + User string + Environment map[string]string Location Location @@ -95,6 +98,9 @@ type Bind struct { Source string Target string CreateHostPath bool + ReadOnly bool + // SELinux is the relabel option: "z", "Z" or "". + SELinux string } type Build struct { diff --git a/pkg/preflight/registry/registry.go b/pkg/preflight/registry/registry.go index 3dbb079..81ca37c 100644 --- a/pkg/preflight/registry/registry.go +++ b/pkg/preflight/registry/registry.go @@ -34,6 +34,9 @@ type Image struct { // CompressedSize is the download size for the requested platform, or 0 // when that platform is not available. CompressedSize int64 + + // User is the image's configured USER for the requested platform. + User string } var ( @@ -151,6 +154,9 @@ func (r *Remote) resolve(ctx context.Context, ref, platform string) (*Image, err if match != nil { if img, err := idx.Image(match.Digest); err == nil { out.CompressedSize = layerSize(img) + if cfg, err := img.ConfigFile(); err == nil { + out.User = cfg.Config.User + } } } return out, nil @@ -167,6 +173,7 @@ func (r *Remote) resolve(ctx context.Context, ref, platform string) (*Image, err p := Normalize(cfg.OS + "/" + cfg.Architecture + "/" + cfg.Variant) out.Platforms = []string{p} out.CompressedSize = layerSize(img) + out.User = cfg.Config.User return out, nil } diff --git a/pkg/preflight/registry/user_live_test.go b/pkg/preflight/registry/user_live_test.go new file mode 100644 index 0000000..05b0029 --- /dev/null +++ b/pkg/preflight/registry/user_live_test.go @@ -0,0 +1,24 @@ +package registry + +import ( + "context" + "os" + "testing" + "time" +) + +func TestLiveImageUser(t *testing.T) { + if os.Getenv("PREFLIGHT_LIVE") == "" { + t.Skip("set PREFLIGHT_LIVE=1") + } + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + r := NewRemote() + for _, ref := range []string{"grafana/grafana:11.2.0", "postgres:16", "docker.elastic.co/elasticsearch/elasticsearch:8.15.0", "jenkins/jenkins:lts"} { + img, err := r.Resolve(ctx, ref, "linux/arm64") + if err != nil { + t.Fatal(err) + } + t.Logf("%-55s user=%q", ref, img.User) + } +} diff --git a/pkg/preflight/rules/owner_unix.go b/pkg/preflight/rules/owner_unix.go new file mode 100644 index 0000000..934cfa3 --- /dev/null +++ b/pkg/preflight/rules/owner_unix.go @@ -0,0 +1,17 @@ +//go:build !windows + +package rules + +import ( + "os" + "syscall" +) + +// owner returns the numeric owner of a file. +func owner(fi os.FileInfo) (int, bool) { + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, false + } + return int(st.Uid), true +} diff --git a/pkg/preflight/rules/owner_windows.go b/pkg/preflight/rules/owner_windows.go new file mode 100644 index 0000000..b3d1a6d --- /dev/null +++ b/pkg/preflight/rules/owner_windows.go @@ -0,0 +1,5 @@ +package rules + +import "os" + +func owner(os.FileInfo) (int, bool) { return 0, false } diff --git a/pkg/preflight/rules/permissions.go b/pkg/preflight/rules/permissions.go new file mode 100644 index 0000000..9e4792b --- /dev/null +++ b/pkg/preflight/rules/permissions.go @@ -0,0 +1,112 @@ +package rules + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/raphgm/container-doctor/pkg/preflight/fact" + "github.com/raphgm/container-doctor/pkg/preflight/project" +) + +// containerUser returns the user a service's processes start as: the +// Compose `user:` value, else the image's USER. uid is -1 when the user is +// a name preflight cannot resolve without unpacking the image. +func containerUser(env *Env, svc *project.Service) (user string, uid int) { + user = svc.User + if user == "" { + ref := serviceImage(svc) + if ref != "" { + want := hostPlatform(env.Host) + if svc.Platform != "" { + want = svc.Platform + } + if res, ok := env.Images[ImageKey(ref, want)]; ok && res.Image != nil { + user = res.Image.User + } + } + } + name, _, _ := strings.Cut(user, ":") + switch name { + case "", "root", "0": + return user, 0 + case "nobody": + return user, 65534 + } + if n, err := strconv.Atoi(name); err == nil { + return user, n + } + return user, -1 +} + +var permissionsRule = rule{ + id: "mounts.permissions", + title: "Containers can write to their bind mounts", + needs: []fact.ID{fact.LocalDaemon, fact.ComposeFile}, + check: func(_ context.Context, env *Env) []Finding { + h := env.Host + var out []Finding + for _, svc := range env.Project.Services { + user, uid := containerUser(env, svc) + for _, b := range svc.Binds { + src := rel(env, b.Source) + + if h.SELinux && b.SELinux == "" { + out = append(out, Finding{ + Rule: "mounts.permissions", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("SELinux will block access to bind mount %s", src), + Evidence: []string{"host: SELinux is enabled for Docker", "project: " + src + " is mounted without :z or :Z"}, + Fix: fmt.Sprintf("Mount it as %s:%s:z (shared) or :Z (private to this container).", src, b.Target), + Location: svc.Location.String(), + Predicts: "Permission denied", + }) + } + + if b.ReadOnly || uid == 0 || h.MapsOwnership() { + continue + } + who := fmt.Sprintf("user %q", user) + if uid >= 0 { + who = fmt.Sprintf("uid %d", uid) + } + + fi, err := os.Stat(b.Source) + if err != nil { + if !b.CreateHostPath { + continue // mounts.bind reports this + } + out = append(out, Finding{ + Rule: "mounts.permissions", + Severity: fact.Error, + Service: svc.Name, + Title: fmt.Sprintf("Docker will create %s owned by root, but the container runs as %s", src, who), + Evidence: []string{"host: " + src + " does not exist; Docker creates missing bind sources as root", "image: runs as " + who}, + Fix: fmt.Sprintf("Create %s yourself and `chown` it to the container's uid, or use a named volume.", src), + Location: svc.Location.String(), + Predicts: "Permission denied", + }) + continue + } + own, ok := owner(fi) + if !ok || uid < 0 || own == uid || fi.Mode().Perm()&0o002 != 0 { + continue + } + out = append(out, Finding{ + Rule: "mounts.permissions", + Severity: fact.Warning, + Service: svc.Name, + Title: fmt.Sprintf("%s is owned by uid %d, but the container writes as uid %d", src, own, uid), + Evidence: []string{fmt.Sprintf("host: %s owner %d, mode %s", src, own, fi.Mode().Perm()), "image: runs as " + who}, + Fix: fmt.Sprintf("`sudo chown -R %d %s`, run the service with `user: \"%d\"`, or mount it read-only if it only reads.", uid, src, own), + Location: svc.Location.String(), + Predicts: "Permission denied", + }) + } + } + return out + }, +} diff --git a/pkg/preflight/rules/rule.go b/pkg/preflight/rules/rule.go index da2786e..acef8e8 100644 --- a/pkg/preflight/rules/rule.go +++ b/pkg/preflight/rules/rule.go @@ -88,6 +88,7 @@ func All() []Rule { portConflictRule, hostPortRule, bindRule, + permissionsRule, fileSharingRule, } } diff --git a/pkg/preflight/rules/rules_test.go b/pkg/preflight/rules/rules_test.go index 4edf233..28dbf5e 100644 --- a/pkg/preflight/rules/rules_test.go +++ b/pkg/preflight/rules/rules_test.go @@ -77,6 +77,9 @@ func (f fakeRegistry) Resolve(_ context.Context, ref, platform string) (*registr return nil, fmt.Errorf("%w: %s", registry.ErrUnauthorized, ref) } img := ®istry.Image{Ref: ref, Index: true, CompressedSize: 500 << 20} + if u, rest, ok := strings.Cut(v, "|user="); ok { + v, img.User = u, rest + } if s, ok := strings.CutPrefix(v, "single:"); ok { img.Index, v = false, s } @@ -423,3 +426,52 @@ func TestDiskUsesSmallerOfVMAndHost(t *testing.T) { t.Errorf("evidence = %v", fs[0].Evidence) } } + +func TestPermissions(t *testing.T) { + p := load(t, map[string]string{ + "compose.yaml": `services: + grafana: + image: grafana/grafana:11 + volumes: ["./grafana-data:/var/lib/grafana", "./conf:/etc/grafana:ro"] + es: + image: elasticsearch:8 + volumes: ["./es-data:/usr/share/elasticsearch/data"] + pg: + image: postgres:16 + volumes: ["./pg-data:/var/lib/postgresql/data"] + app: + image: alpine + user: "1000:1000" + volumes: ["./shared:/shared"] +`, + "es-data/.keep": "", + "shared/.keep": "", + }) + if err := os.Chmod(filepath.Join(p.Dir, "shared"), 0o777); err != nil { + t.Fatal(err) + } + reg := fakeRegistry{ + "grafana/grafana:11": multiArch + "|user=472", + "elasticsearch:8": multiArch + "|user=1000:0", + "postgres:16": multiArch, + "alpine": multiArch, + } + linux := oldLinux() + linux.Platform, linux.ClientArch = "linux/amd64", "amd64" + fs := run(t, permissionsRule, p, linux, reg) + expect(t, fs, fact.Error, "Docker will create grafana-data owned by root, but the container runs as uid 472") + if os.Getuid() != 1000 { + expect(t, fs, fact.Warning, "es-data is owned by uid") + } + for _, bad := range []string{"pg-data", "conf", "shared"} { + if strings.Contains(titles(fs), bad) { + t.Errorf("%s must not be flagged (root image / read-only / world-writable):\n%s", bad, titles(fs)) + } + } + + expectNone(t, run(t, permissionsRule, p, armDesktop(), reg)) + + selinux := oldLinux() + selinux.SELinux = true + expect(t, run(t, permissionsRule, p, selinux, reg), fact.Error, "SELinux will block access to bind mount conf") +} From 9a6be8d6157c26053dee6c9635f7d05bdae08902 Mon Sep 17 00:00:00 2001 From: raphgm Date: Thu, 24 Sep 2026 02:50:59 +0100 Subject: [PATCH 07/23] feat(preflight): read Docker Desktop's VM; diagnose installer-disk launches On macOS the Desktop VM's command line shows whether amd64 runs under Rosetta or QEMU and which folders are shared, so mounts.sharing uses the real shared paths (now an error, matching Docker's 'mounts denied') and emulation findings know the mechanism. Docker Desktop started from its mounted installer image is diagnosed as the root cause of a hung daemon. Validated against Docker Desktop 29.8 on Apple silicon. --- docs/validation/2026-09-23-colima-m-series.md | 20 ++++++++ pkg/preflight/host/diagnose.go | 13 +++++ pkg/preflight/host/diagnose_test.go | 7 +++ pkg/preflight/host/host.go | 50 +++++++++++++++++++ pkg/preflight/rules/build.go | 2 +- pkg/preflight/rules/host_io.go | 14 ++++-- pkg/preflight/rules/resources.go | 18 +++++++ pkg/preflight/rules/rule.go | 1 + 8 files changed, 119 insertions(+), 6 deletions(-) diff --git a/docs/validation/2026-09-23-colima-m-series.md b/docs/validation/2026-09-23-colima-m-series.md index 316b21a..71f7dff 100644 --- a/docs/validation/2026-09-23-colima-m-series.md +++ b/docs/validation/2026-09-23-colima-m-series.md @@ -65,3 +65,23 @@ Following the fix started a working daemon. The first `colima start` failed beca the password was removed on another tag (`postgres:17`). - `memory=<2GiB` remains a spurious condition until a failure on a larger VM generalizes it away. This bias of single-example learning should be discussed in the paper. + +## Same Mac on Docker Desktop (2026-09-24) + +Host: Docker Desktop, engine 29.8.0, VM 3.8 GiB, Rosetta enabled, buildx 0.37.1, Compose 5.5.1. +The same project (`broken-shop`) got different predictions than it did on Colima, and each +difference was confirmed: + +| Check | Colima prediction | Desktop prediction | Real on Desktop | +|---|---|---|---| +| credential helper | error | passes (helper installed) | pulls work | +| `RUN --mount` | error (no buildx) | passes | builds | +| mssql | error: crashes under QEMU | warning: Rosetta emulation | `SQL Server is now ready for client connections` ✅ | +| bind outside shared paths | n/a | error (shared paths read from the VM) | `mounts denied: The path /opt/homebrew/etc is not shared from the host` ✅ exact | +| missing COPY source | legacy wording | BuildKit wording | `failed to compute cache key: failed to calculate checksum of ref …: "/missing.txt": not found` (text corrected) | + +A new failure was found during setup and is now diagnosed. Docker Desktop had been started from +its installer disk image. That copy kept the socket and hung every command, while the copy in +/Applications attached to it. Preflight first reported only "accepts connections but does not +answer". It now names the installer copy as the root cause and adds a `host.installer` warning. +Probe commands also time out after 20s, where before they hung. diff --git a/pkg/preflight/host/diagnose.go b/pkg/preflight/host/diagnose.go index 2d90e26..ac387ea 100644 --- a/pkg/preflight/host/diagnose.go +++ b/pkg/preflight/host/diagnose.go @@ -28,6 +28,10 @@ type daemonClues struct { Installed []engine Running []engine + // FromInstaller is true when Docker Desktop processes run from a + // mounted installer disk image. + FromInstaller bool + // DiskFree is free space on this machine; VM engines cannot start // without room for their disk image. DiskFree int64 @@ -50,6 +54,11 @@ func diagnoseDaemon(c daemonClues) fact.Failure { } switch { + case c.FromInstaller: + f.Summary = "Docker Desktop is running from its installer disk image and holds the socket" + f.Fix = "Quit it, stop leftover processes (`pkill -f /Volumes/Docker/Docker.app`), eject the Docker disk, then start Docker from Applications." + return f + case strings.Contains(c.Err, "did not answer within"): f.Summary = "Docker daemon accepts connections but does not answer" fix := "Wait for it to finish starting; if it stays stuck, restart it." @@ -156,6 +165,10 @@ func gatherClues(ctxName, endpoint, errText string, diskFree int64) daemonClues }{engine{"Docker Engine", "/var/run/docker.sock", "sudo systemctl start docker", "default"}, func() bool { return onPath("dockerd") }}) } + if out, err := osexec.Command("pgrep", "-f", "/Volumes/.*/Docker.app/").Output(); err == nil && len(strings.TrimSpace(string(out))) > 0 { + c.FromInstaller = true + } + for _, cand := range candidates { if fileExists(cand.Socket) { c.Running = append(c.Running, cand.engine) diff --git a/pkg/preflight/host/diagnose_test.go b/pkg/preflight/host/diagnose_test.go index 8097530..b6006c5 100644 --- a/pkg/preflight/host/diagnose_test.go +++ b/pkg/preflight/host/diagnose_test.go @@ -85,3 +85,10 @@ func TestDiagnoseHungDaemonWithTwoVMs(t *testing.T) { t.Errorf("got %q / %q", f.Summary, f.Fix) } } + +func TestDiagnoseInstallerCopy(t *testing.T) { + f := diagnoseDaemon(daemonClues{Endpoint: "unix:///h/.docker/run/docker.sock", Err: "did not answer within 20s", FromInstaller: true}) + if !strings.Contains(f.Summary, "installer disk image") || !strings.Contains(f.Fix, "eject") { + t.Errorf("got %q / %q", f.Summary, f.Fix) + } +} diff --git a/pkg/preflight/host/host.go b/pkg/preflight/host/host.go index 3a97cf9..b86680a 100644 --- a/pkg/preflight/host/host.go +++ b/pkg/preflight/host/host.go @@ -71,6 +71,10 @@ type Profile struct { // "rosetta". Some software runs under Rosetta but crashes under QEMU. Emulators map[string]string + // SharedPaths are the host folders Docker Desktop shares with its VM, + // read from the running VM. Empty when unknown. + SharedPaths []string + // Sysctls holds kernel settings read on a local Linux daemon host. Sysctls map[string]int64 @@ -85,6 +89,10 @@ type Profile struct { PortProbe func(ip string, port int, proto string) bool `json:"-"` Lsof func(port int, proto string) string `json:"-"` + // RunningFromInstaller is true when Docker Desktop runs from its + // mounted installer disk image instead of /Applications. + RunningFromInstaller bool + // Local is true when the daemon endpoint is on this machine. Local bool @@ -235,6 +243,7 @@ func (p *Profile) probeDaemon(ctx context.Context, run executor.Runner, facts *f p.Sysctls = map[string]int64{"vm.max_map_count": 262144} p.EmulationKnown = true p.DiskExact = false + p.readDesktopVM(ctx, run) } else if shell := p.kernelShell(); shell != nil { p.readKernel(ctx, run, shell) } @@ -280,6 +289,47 @@ func (p *Profile) probeEndpoint(ctx context.Context, run executor.Runner, facts } } +// readDesktopVM reads Docker Desktop's VM command line on macOS: it shows +// whether amd64 runs under Rosetta or QEMU and which folders are shared. +func (p *Profile) readDesktopVM(ctx context.Context, run executor.Runner) { + if p.ClientOS != "darwin" { + return + } + pid, err := run.Run(ctx, "pgrep", "-f", "com.docker.virtualization") + if err != nil || pid == "" { + return + } + args, err := run.Run(ctx, "ps", "-o", "args=", "-p", strings.Fields(pid)[0]) + if err != nil { + return + } + fields := strings.Fields(args) + emulator := "qemu" + for i, f := range fields { + switch f { + case "--rosetta": + emulator = "rosetta" + case "--virtiofs": + if i+1 < len(fields) { + p.SharedPaths = append(p.SharedPaths, fields[i+1]) + } + } + } + for _, e := range p.Emulated { + if p.Emulators == nil { + p.Emulators = map[string]string{} + } + if e == "linux/amd64" { + p.Emulators[e] = emulator + } else { + p.Emulators[e] = "qemu" + } + } + if strings.Contains(args, "/Volumes/") && strings.Contains(args, "Docker.app") { + p.RunningFromInstaller = true + } +} + // kernelShell returns the command prefix that runs a shell where the // daemon's kernel lives: this machine for a local Linux daemon, or the VM // for Colima. It returns nil when there is no way in. diff --git a/pkg/preflight/rules/build.go b/pkg/preflight/rules/build.go index 0577842..390638a 100644 --- a/pkg/preflight/rules/build.go +++ b/pkg/preflight/rules/build.go @@ -56,7 +56,7 @@ var buildContextRule = rule{ // The two builders word a missing COPY source differently. notFound := func(src string) string { if env.Host.BuildxInstalled && !env.Host.BuildKitDisabled { - return `failed to compute cache key: "/` + strings.TrimPrefix(src, "/") + `": not found` + return `failed to compute cache key: failed to calculate checksum of ref …: "/` + strings.TrimPrefix(src, "/") + `": not found` } return "COPY failed: file not found in build context or excluded by .dockerignore: stat " + strings.TrimPrefix(src, "/") + ": file does not exist" } diff --git a/pkg/preflight/rules/host_io.go b/pkg/preflight/rules/host_io.go index 83564ac..1a6b203 100644 --- a/pkg/preflight/rules/host_io.go +++ b/pkg/preflight/rules/host_io.go @@ -186,8 +186,12 @@ var fileSharingRule = rule{ var out []Finding for _, svc := range env.Project.Services { for _, b := range svc.Binds { + paths, source := desktopSharedPaths, "shares "+strings.Join(desktopSharedPaths, ", ")+" by default" + if len(h.SharedPaths) > 0 { + paths, source = h.SharedPaths, "shares "+strings.Join(h.SharedPaths, ", ")+" (read from the running VM)" + } shared := false - for _, p := range desktopSharedPaths { + for _, p := range paths { if b.Source == p || strings.HasPrefix(b.Source, p+"/") { shared = true } @@ -196,12 +200,12 @@ var fileSharingRule = rule{ continue } out = append(out, Finding{ - Rule: "mounts.sharing", Severity: fact.Warning, Service: svc.Name, - Title: "bind source " + b.Source + " is outside Docker Desktop's default shared paths", - Evidence: []string{"host: Docker Desktop shares " + strings.Join(desktopSharedPaths, ", ") + " by default"}, + Rule: "mounts.sharing", Severity: fact.Error, Service: svc.Name, + Title: "bind source " + b.Source + " is outside Docker Desktop's shared paths", + Evidence: []string{"host: Docker Desktop " + source}, Fix: "Add the path in Docker Desktop → Settings → Resources → File sharing.", Location: svc.Location.String(), - Predicts: "Mounts denied: The path " + b.Source + " is not shared from the host and is not known to Docker.", + Predicts: "mounts denied: The path " + b.Source + " is not shared from the host and is not known to Docker.", }) } } diff --git a/pkg/preflight/rules/resources.go b/pkg/preflight/rules/resources.go index f2bba76..f3e3b91 100644 --- a/pkg/preflight/rules/resources.go +++ b/pkg/preflight/rules/resources.go @@ -179,6 +179,24 @@ const ( vmWarnFree = 10 << 30 ) +var installerRule = rule{ + id: "host.installer", + title: "Docker Desktop runs from Applications", + needs: []fact.ID{fact.Daemon}, + check: func(_ context.Context, env *Env) []Finding { + if !env.Host.RunningFromInstaller { + return nil + } + return []Finding{{ + Rule: "host.installer", + Severity: fact.Warning, + Title: "Docker Desktop is running from its installer disk image", + Evidence: []string{"host: the Docker VM was started from /Volumes/…/Docker.app"}, + Fix: "Quit Docker Desktop, drag it to Applications, eject the Docker disk and start it from Applications. A copy started from the disk can keep the socket and hang every command.", + }} + }, +} + var hostDiskRule = rule{ id: "host.disk", title: "This machine has room for Docker's VM", diff --git a/pkg/preflight/rules/rule.go b/pkg/preflight/rules/rule.go index acef8e8..dd45b05 100644 --- a/pkg/preflight/rules/rule.go +++ b/pkg/preflight/rules/rule.go @@ -83,6 +83,7 @@ func All() []Rule { memoryRule, diskRule, hostDiskRule, + installerRule, gpuRule, sysctlRule, portConflictRule, From 2ebcb6c3a26501e90b2cffa728e00f6b37b4bf9e Mon Sep 17 00:00:00 2001 From: raphgm Date: Thu, 24 Sep 2026 02:53:34 +0100 Subject: [PATCH 08/23] bench: stratified sample of 124 issues with first-rater labels --- bench/__pycache__/precision.cpython-314.pyc | Bin 0 -> 6940 bytes bench/labels/labels_claude.tsv | 125 ++++++++++++++++++++ bench/labels/sample.jsonl | 124 +++++++++++++++++++ bench/sample.py | 48 ++++++++ 4 files changed, 297 insertions(+) create mode 100644 bench/__pycache__/precision.cpython-314.pyc create mode 100644 bench/labels/labels_claude.tsv create mode 100644 bench/labels/sample.jsonl create mode 100644 bench/sample.py diff --git a/bench/__pycache__/precision.cpython-314.pyc b/bench/__pycache__/precision.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f9f77247a76d04fd713620d90587d0325c5478a GIT binary patch literal 6940 zcmbU_TWs4{c9#{2IaRQ-bL?Z<~ zL^N@ePza;9C>Eo)DpsQ>71HQ!ip}Wliam;G3vG=Mvjk1jHlw%G4x@L{E~9tT9;5ft zbw*!LHyC{*-DLDf=w_pDp<9i2JLY=4DAW}B(wwcDQF{f6x!4D8TxF*LJ!hI0G*@HLpw}gfc7Fi0_`Y0 z2JIz!oW2ZPSLg|VuF}^cM6}@=p*ZO4KBPG58_>EQxar9Y-$Ez_77m~Xt{@sJ8?*wXfngf)S@7zWQrFAndO%TC|RH+W)URjb0A+> zU?o`rfo@S^7Z{$+CD{lyDM%Cx6wrmqiaeVtNgkk-3@1}_Y)P<^VX0XmwL~$w6v)eF zSu7C%H_V%y2SyNsnQ9e&!X%ZF+LXdRQ6dz~o8)9p$WfOln39|0V7jO|@qil(XHt*y zvI6)^lp-h$PyNGZ)o~H`9ko;B83m@xj!^6qHi>73<(KABo8ClqSNU@SSREF`<55b}!5 zrNGm$N)>9mi8h?@5XoQG;3~#WaSFuH<2(m;P@Bqca!J@cr>Grx1rQk{6)tLlkvL|S zXVoL%>t$n6450ZutF~A8O*Uf7 z5JxCumP!QF9R{1SY!)USq3+(jJ;m|hvbswUvh&8P+Gviz8bjm~k|d{*^A+Td9R z0`%96qa`IIVHT=1NNr`adB~mMWFypLc!xZ|$Qd<&-$x+rB$<3V zqd;gj!Q?`+(I=wK$0oRQ9}^NtONli zQ6eMD`0$0pv$W$PoOJkjOTw{Vd7SWOh2N_o3$C={C{{}?v(Zp7U^;%@O1IFKhr|$; z1CK~VkZ8HzL|X?+2%{aeG@{9W1gdqWED2)kose`g#VCw!;~;~`x&?}5?05ZC>Cvx8KY$pNqY@)#7?y`Dekv1p=PiU4=!Bf9B zx;m=)2DUOgp5cAZ<*z-LU%0=zrG4we9nVM0*9+eImM756S z7QH>R*FOCG?BAdNoAWRF|8{(T^sY8~_bYm@J@&8Od#|jh+5fT*)ejXLk+*T};_Aiq z4|Y7A`<~#}p5Trrq>-Uluuhc3jrri9h0s8+cpE~Ddl}7`6cno@lnM&Pfa~?=w~TPS z&R?5M%wV9AnF>S%X^xojn4@MYY2|GRCiwr*H(rx=&H2r~5#1mr{}2+6Wv1TLAXZ6N zCI%sxw~~&kA#F;5WZYu%#;vqvpu>a^s(lyX^IC+Y3@N>a15#cFG_ccT+|kAAN&E-gW~Q%g0}k*?~Id;m5I-?G~KKXy@%lNbGMNYt9L}ko_>}{Wwk5>NHSiY62^lk zsOaRv+TXuFQ1JzXZo@?djCJ8TXS!;nxoc;~9JTO3jn|qp0guO!Qr+inQ(m=)^;>Iq zn)aH0`jDXO%tI6`OvHlKQRW-WacJ)WuCLV#;6r-f&mUUTp*1|Tm@{L|Yo!c2%r*6( z8JyCL=XEZ+p$^fFaU0zP{gJqXZic=ExDILPMQ0r|eWr{UHqdJi-8x*(3q24kQ_spf zTZx9>k_~Eh!W0bg*k-Pc_R>DuZ`yT~NDz5T-kPZRtmvFMZjOtcFyE>@>{sRR7hQ2z z>{OLzzC{onxJy)R23+_McuDd!CE~?1LL3q9lJIq$YCjP%PE)6Op8^M}aXq6u&5IKWCx=7jCC=D`@Oy?03bPV-euTnS-4Z#i6Iq>5WGt$m zT9$gD*Ii?2HV0pX(zx!+!qrq{41as_@_3{|m;^o<@Ahwg0ds2oV}GG+2e&5veBm#Z z|B^|-|6WrIF*&WfDn~998u)rgcbXSIIw;zvve9WH<{*1!1a@Hc^LNV z7D1M9(v)y!*B$WPjlZIFXCi@Zl1PBF7}pCrndXIAsR?tDxGvL4xNgYlG>wj0>qhwtVh*=J>qfS;LQ=JN`4<=XX3KP?34+maiW)wtb>} zw)EYlKRxoL?+f3TfiD7EsqJ@nf|quD>KfOH_2Zv< zcD#WV;^*NL8~z^zwuu#D<>YGpfxl~m*>rsW(bk!*kAE`0*L`uvKLVr^t91uX@0#nW zYd!eM^hV#N^?Qk}dwb4k0kED`&-%S}_0tEN6Pt5?c600Tw(lqE^E3Z!`-;(GAOA~E zdzjdH_jk0}q?XF<%?etJxa)j;;H+D3S|9neYeUi+dpG;H{M)@-b=&v0A3r~`J@Zf z^Zc(J$UQ;8Q277Gb>+WhTr>Q%X~J=<*Y;1no?C-1?C01o5()4%M3^yA5F)etn z*o5ID2K|cGH&JZHa0{~aXu%Cj#*6J3?m)Iat$+0S z1MTQku@hqg$kwIxjA-4X#V!nY8}P8!eWBQcVak}{ibhQogBb2bwhk>Ys&$SPLm2Ku zwqC98h8CJEkb#Zg)w-hv(zkJPqpLu6YTd(H*M$Pvt_99%o#zeAxYl*KKz3`v!R-$< z3SI+R&vmW)hCv+Dx-J<+yduNUx3thyfjqiZuN@o4g2S61Ywc$WWS2%=1hos~G3~@% tEgX9}L4Y>P