Files
wehub-resource-sync 36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:06 +08:00

188 lines
5.3 KiB
Go

package proc
import (
"context"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/pranshuparmar/witr/pkg/model"
)
const runtimeQueryTimeout = 3 * time.Second
var (
healthRe = regexp.MustCompile(`\(([^)]+)\)\s*$`)
dockerCreatedAtLayouts = []string{
"2006-01-02 15:04:05 -0700 MST",
"2006-01-02 15:04:05.999999999 -0700 MST",
"2006-01-02 15:04:05 -0700 -0700",
time.RFC3339Nano,
time.RFC3339,
}
)
// rootlessBins are runtime binaries whose container state is per-user
// (rootless). When witr runs under sudo, we drop privileges back to the
// original user for these so the user's containers stay visible.
var rootlessBins = map[string]bool{
"podman": true,
"nerdctl": true,
}
func dockerLikeList(bin, runtime string) []*model.ContainerMatch {
ctx, cancel := context.WithTimeout(context.Background(), runtimeQueryTimeout)
defer cancel()
// {{.Labels}} returns the full label map as comma-separated key=value
// pairs. The {{.Label "key"}} form is Docker-specific and fails the
// template parser on Podman, so we read all labels and pick out the
// compose ones ourselves.
format := strings.Join([]string{
"{{.ID}}",
"{{.Names}}",
"{{.Image}}",
"{{.Command}}",
"{{.State}}",
"{{.Status}}",
"{{.CreatedAt}}",
"{{.Networks}}",
"{{.Mounts}}",
"{{.Ports}}",
"{{.Labels}}",
}, "|")
out, err := runtimeCommand(ctx, bin, "ps", "--no-trunc", "--format", format).Output()
if err != nil {
return nil
}
var matches []*model.ContainerMatch
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.SplitN(line, "|", 11)
if len(parts) < 11 {
continue
}
labels := parseLabelString(parts[10])
matches = append(matches, &model.ContainerMatch{
Runtime: runtime,
ID: parts[0],
Name: parts[1],
Image: parts[2],
Command: strings.Trim(parts[3], "\""),
State: parts[4],
Status: parts[5],
Health: healthFromStatus(parts[5]),
CreatedAt: parseDockerTime(parts[6]),
Networks: parts[7],
Mounts: parts[8],
Ports: parts[9],
ComposeProject: labels["com.docker.compose.project"],
ComposeService: labels["com.docker.compose.service"],
ComposeConfigFile: labels["com.docker.compose.project.config_files"],
ComposeWorkingDir: labels["com.docker.compose.project.working_dir"],
})
}
return matches
}
// parseLabelString turns "key1=val1,key2=val2" into a map. Values with embedded
// commas are uncommon for the labels we care about (paths, project names),
// so the simple split is acceptable for now.
func parseLabelString(s string) map[string]string {
out := map[string]string{}
if s == "" {
return out
}
for _, kv := range strings.Split(s, ",") {
kv = strings.TrimSpace(kv)
if i := strings.Index(kv, "="); i > 0 {
out[kv[:i]] = kv[i+1:]
}
}
return out
}
// healthFromStatus pulls "healthy" / "unhealthy" / "starting" out of a status
// like "Up 4 minutes (healthy)". Returns "" when no health check is wired.
func healthFromStatus(status string) string {
m := healthRe.FindStringSubmatch(status)
if len(m) != 2 {
return ""
}
v := strings.ToLower(strings.TrimSpace(m[1]))
switch v {
case "healthy", "unhealthy", "health: starting", "starting":
return strings.TrimPrefix(v, "health: ")
}
return ""
}
func parseDockerTime(s string) time.Time {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}
}
for _, layout := range dockerCreatedAtLayouts {
if t, err := time.Parse(layout, s); err == nil {
return t
}
}
return time.Time{}
}
func dockerLikeHostPID(bin, id string) int {
ctx, cancel := context.WithTimeout(context.Background(), runtimeQueryTimeout)
defer cancel()
out, err := runtimeCommand(ctx, bin, "inspect", "-f", "{{.State.Pid}}", id).Output()
if err != nil {
return 0
}
pid, _ := strconv.Atoi(strings.TrimSpace(string(out)))
return pid
}
// dockerLikeEnrich fills in the container's actual start time via
// `<bin> inspect --format '{{.State.StartedAt}}'`. The list scan only gives
// us creation time, which is misleading for any container that was stopped
// and restarted later.
func dockerLikeEnrich(bin string, match *model.ContainerMatch) {
if match == nil || match.ID == "" {
return
}
ctx, cancel := context.WithTimeout(context.Background(), runtimeQueryTimeout)
defer cancel()
out, err := runtimeCommand(ctx, bin, "inspect", "-f", "{{.State.StartedAt}}", match.ID).Output()
if err != nil {
return
}
s := strings.TrimSpace(string(out))
if s == "" || s == "0001-01-01T00:00:00Z" {
return
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
match.StartedAt = t
}
}
// runtimeCommand wraps exec.CommandContext but, for rootless-typical runtimes
// (podman, nerdctl), drops privileges back to the original user when invoked
// under sudo. Daemon-based runtimes (docker) are left as-is so users who rely
// on `sudo` for docker socket access still work.
func runtimeCommand(ctx context.Context, bin string, args ...string) *exec.Cmd {
if rootlessBins[bin] {
return commandAsOriginalUser(ctx, bin, args...)
}
return exec.CommandContext(ctx, bin, args...)
}
func binAvailable(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}