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
266 lines
6.8 KiB
Go
266 lines
6.8 KiB
Go
package proc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/pranshuparmar/witr/pkg/model"
|
|
)
|
|
|
|
// ResolveContainerByPort queries the Docker CLI for a container publishing
|
|
// the given port. Returns nil if Docker is unavailable or no container matches.
|
|
func ResolveContainerByPort(port int) *model.ContainerMatch {
|
|
if _, err := exec.LookPath("docker"); err != nil {
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
format := strings.Join([]string{
|
|
"{{.ID}}", "{{.Names}}", "{{.Image}}", "{{.Command}}",
|
|
"{{.State}}", "{{.Status}}", "{{.CreatedAt}}",
|
|
"{{.Networks}}", "{{.Mounts}}", "{{.Ports}}", "{{.Labels}}",
|
|
}, "|")
|
|
cmd := exec.CommandContext(ctx, "docker", "ps", "--no-trunc", "--filter", fmt.Sprintf("publish=%d", port), "--format", format)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
line := strings.TrimSpace(string(out))
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
if idx := strings.Index(line, "\n"); idx >= 0 {
|
|
line = line[:idx]
|
|
}
|
|
|
|
parts := strings.SplitN(line, "|", 11)
|
|
if len(parts) < 11 {
|
|
return nil
|
|
}
|
|
labels := parseLabelString(parts[10])
|
|
|
|
return &model.ContainerMatch{
|
|
Runtime: "docker",
|
|
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"],
|
|
}
|
|
}
|
|
|
|
// isValidContainerID reports whether id is a safe container identifier to hand
|
|
// to a runtime CLI: a non-empty token of [A-Za-z0-9_.-] that begins with an
|
|
// alphanumeric. Rejecting a leading dash (and any other metacharacter) keeps a
|
|
// malformed, cgroup-derived id from being parsed as a CLI option.
|
|
func isValidContainerID(id string) bool {
|
|
if id == "" {
|
|
return false
|
|
}
|
|
for i := 0; i < len(id); i++ {
|
|
c := id[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
|
|
case (c == '_' || c == '.' || c == '-') && i > 0:
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// resolveContainerName attempts to resolve a container ID to a name using the specified runtime CLI.
|
|
func resolveContainerName(id, runtime string) string {
|
|
if !isValidContainerID(id) {
|
|
return ""
|
|
}
|
|
var cmd *exec.Cmd
|
|
var prefix string
|
|
|
|
ctx := context.Background()
|
|
switch runtime {
|
|
case "docker":
|
|
if _, err := exec.LookPath("docker"); err != nil {
|
|
return ""
|
|
}
|
|
cmd = exec.CommandContext(ctx, "docker", "inspect", "--format", "{{.Name}}|{{index .Config.Labels \"com.docker.compose.project\"}}|{{index .Config.Labels \"com.docker.compose.service\"}}", "--", id)
|
|
prefix = "docker: "
|
|
case "podman":
|
|
if _, err := exec.LookPath("podman"); err != nil {
|
|
return ""
|
|
}
|
|
cmd = commandAsOriginalUser(ctx, "podman", "inspect", "--format", "{{.Name}}", "--", id)
|
|
prefix = "podman: "
|
|
case "crictl":
|
|
if _, err := exec.LookPath("crictl"); err != nil {
|
|
return ""
|
|
}
|
|
cmd = exec.CommandContext(ctx, "crictl", "inspect", id, "-o", "go-template", "--template", "{{.status.metadata.name}}")
|
|
prefix = "" // crictl names are usually clean
|
|
case "nerdctl":
|
|
if _, err := exec.LookPath("nerdctl"); err != nil {
|
|
return ""
|
|
}
|
|
cmd = commandAsOriginalUser(ctx, "nerdctl", "inspect", id, "--format", "{{.Name}}")
|
|
prefix = "containerd: "
|
|
default:
|
|
return ""
|
|
}
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
output := strings.TrimSpace(string(out))
|
|
|
|
if runtime == "docker" {
|
|
parts := strings.Split(output, "|")
|
|
if len(parts) == 3 {
|
|
name := strings.TrimPrefix(parts[0], "/")
|
|
project := parts[1]
|
|
service := parts[2]
|
|
|
|
if project != "" && service != "" {
|
|
return "docker: " + project + "/" + service + " (" + name + ")"
|
|
}
|
|
if name != "" {
|
|
return "docker: " + name
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
name := strings.TrimPrefix(output, "/")
|
|
if name != "" {
|
|
if prefix != "" {
|
|
return prefix + name
|
|
}
|
|
return name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ContainerHealthcheckStatus reports whether the container runtime has a
|
|
// healthcheck configured: "present", "absent", or "" when undeterminable
|
|
// (runtime unavailable, inspect error, or unsupported runtime).
|
|
func ContainerHealthcheckStatus(id, runtime string) string {
|
|
if !isValidContainerID(id) || (runtime != "docker" && runtime != "podman") {
|
|
return ""
|
|
}
|
|
if _, err := exec.LookPath(runtime); err != nil {
|
|
return ""
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), runtimeQueryTimeout)
|
|
defer cancel()
|
|
out, err := runtimeCommand(ctx, runtime, "inspect", "--format", "{{if .Config.Healthcheck}}present{{else}}absent{{end}}", "--", id).Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
switch strings.TrimSpace(string(out)) {
|
|
case "present":
|
|
return "present"
|
|
case "absent":
|
|
return "absent"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// findLongHexID searches for a 64-character hexadecimal string in the input.
|
|
func findLongHexID(s string) string {
|
|
for i := 0; i <= len(s)-64; i++ {
|
|
if s[i] < '0' || (s[i] > '9' && s[i] < 'a') {
|
|
continue
|
|
}
|
|
sub := s[i : i+64]
|
|
isHex := true
|
|
for j := 0; j < 64; j++ {
|
|
c := sub[j]
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
|
isHex = false
|
|
break
|
|
}
|
|
}
|
|
if isHex {
|
|
return sub
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// shortID returns the first 12 characters of a container ID, or the full
|
|
// string if it is shorter than 12 characters.
|
|
func shortID(id string) string {
|
|
if len(id) > 12 {
|
|
return id[:12]
|
|
}
|
|
return id
|
|
}
|
|
|
|
// extractFlagValue extracts the value of a specific flag from a command line string.
|
|
func extractFlagValue(cmdline string, flags ...string) string {
|
|
args := splitCmdline(cmdline)
|
|
for i, arg := range args {
|
|
for _, flag := range flags {
|
|
if arg == flag && i+1 < len(args) {
|
|
return args[i+1]
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitCmdline splits a command line string into arguments, handling quotes and escapes.
|
|
func splitCmdline(cmdline string) []string {
|
|
var args []string
|
|
var current strings.Builder
|
|
var quote rune
|
|
escaped := false
|
|
for _, r := range cmdline {
|
|
switch {
|
|
case escaped:
|
|
current.WriteRune(r)
|
|
escaped = false
|
|
case r == '\\':
|
|
escaped = true
|
|
case r == '"' || r == '\'':
|
|
if quote == 0 {
|
|
quote = r
|
|
continue
|
|
}
|
|
if quote == r {
|
|
quote = 0
|
|
continue
|
|
}
|
|
current.WriteRune(r)
|
|
case unicode.IsSpace(r) && quote == 0:
|
|
if current.Len() > 0 {
|
|
args = append(args, current.String())
|
|
current.Reset()
|
|
}
|
|
default:
|
|
current.WriteRune(r)
|
|
}
|
|
}
|
|
if current.Len() > 0 {
|
|
args = append(args, current.String())
|
|
}
|
|
return args
|
|
}
|