package docker import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/docker/docker/client" ) // EngineInfo — runtime yang benar-benar terdeteksi (Docker ATAU Podman). type EngineInfo struct { Name string // "docker" | "podman" Host string // unix:///… atau tcp://… (kosong = default/env DOCKER_HOST) } // candidate — endpoint yang dicoba berurutan saat deteksi. type candidate struct { name string host string // kosong = biarkan env/DOCKER_HOST yang menentukan } // engineCandidates — urutan kandidat: override config → DOCKER_HOST → // docker default → podman rootless (XDG) → podman rootful. func engineCandidates(override string) []candidate { switch { case override != "": return []candidate{{name: nameFromHost(override), host: override}} case os.Getenv("DOCKER_HOST") != "": h := os.Getenv("DOCKER_HOST") return []candidate{{name: nameFromHost(h), host: ""}} } out := []candidate{{name: "docker", host: ""}} if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { out = append(out, candidate{ name: "podman", host: "unix://" + filepath.Join(xdg, "podman", "podman.sock"), }) } out = append(out, candidate{name: "podman", host: "unix:///run/podman/podman.sock"}) return out } func nameFromHost(host string) string { if strings.Contains(host, "podman") { return "podman" } return "docker" } // Detect — client pertama yang ping-nya hidup, plus info runtime-nya. // Podman mengekspos API kompatibel-Docker di podman.sock, jadi client yang // sama dipakai untuk keduanya. func Detect(override string) (*Client, EngineInfo, error) { connect := func(host string) (*Client, error) { opts := []client.Opt{client.WithAPIVersionNegotiation()} if host == "" { opts = append(opts, client.FromEnv) } else { opts = append(opts, client.WithHost(host)) } cli, err := client.NewClientWithOpts(opts...) if err != nil { return nil, err } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if _, err := cli.Ping(ctx); err != nil { _ = cli.Close() return nil, err } return &Client{cli: cli}, nil } return detectWith(engineCandidates(override), connect) } // detectWith — inti deteksi; connect dapat di-stub di unit test. func detectWith(cands []candidate, connect func(host string) (*Client, error)) (*Client, EngineInfo, error) { var lastErr error for _, c := range cands { cl, err := connect(c.host) if err == nil { return cl, EngineInfo{Name: c.name, Host: c.host}, nil } lastErr = err } return nil, EngineInfo{}, fmt.Errorf("engine: tidak ada Docker/Podman yang merespons: %w", lastErr) } // New — pintasan auto-detect (kompatibilitas; info runtime diabaikan). func New() (*Client, error) { cl, _, err := Detect("") return cl, err }