From e9a638f61cdf9bdda4025ce18a8812dee91c3544 Mon Sep 17 00:00:00 2001 From: Alexandru Vizitiu Date: Wed, 8 Oct 2025 14:04:29 +0300 Subject: [PATCH] Add support for Podman --- cgroup/cgroup.go | 28 ++++ containers/podman.go | 281 +++++++++++++++++++++++++++++++++++++++++ containers/registry.go | 15 ++- 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 containers/podman.go diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go index 1dd1df5..c9c6ac0 100644 --- a/cgroup/cgroup.go +++ b/cgroup/cgroup.go @@ -26,6 +26,8 @@ var ( systemSliceIdRegexp = regexp.MustCompile(`(/(system|runtime|reserved)\.slice/([^/]+))`) talosIdRegexp = regexp.MustCompile(`/(system|podruntime)/([^/]+)`) lxcPayloadRegexp = regexp.MustCompile(`/lxc\.payload\.([^/]+)`) + libpodIdRegexp = regexp.MustCompile(`libpod-(?:conmon-)?([a-z0-9]{64})`) + containerIdRegexp = regexp.MustCompile(`[a-f0-9]{64}`) ) type ContainerType uint8 @@ -40,6 +42,7 @@ const ( ContainerTypeSystemdService ContainerTypeSandbox ContainerTypeTalosRuntime + ContainerTypeLibpod ) func (t ContainerType) String() string { @@ -56,6 +59,8 @@ func (t ContainerType) String() string { return "lxc" case ContainerTypeSystemdService: return "systemd" + case ContainerTypeLibpod: + return "libpod" default: return "unknown" } @@ -211,6 +216,29 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) { return ContainerTypeUnknown, "", fmt.Errorf("invalid lxc payload cgroup %s", cgroupPath) } return ContainerTypeLxc, "/lxc/" + matches[1], nil + case prefix == "machine.slice": + // Handle Podman/libpod containers + // Pattern: /machine.slice/libpod-.scope or /machine.slice/libpod-conmon-.scope + if strings.Contains(cgroupPath, "libpod-") { + // Extract the libpod container ID + matches := libpodIdRegexp.FindStringSubmatch(cgroupPath) + if matches != nil && len(matches) > 1 { + // The first capture group contains the container ID + klog.V(4).Infof("Detected Podman container in machine.slice: %s -> %s", cgroupPath, matches[1]) + return ContainerTypeLibpod, matches[1], nil + } + } + case prefix == "user.slice": + // Check if this might be a Podman container running in user session + // Look for libpod patterns in user slices as well + if strings.Contains(cgroupPath, "libpod-") { + matches := libpodIdRegexp.FindStringSubmatch(cgroupPath) + if matches != nil && len(matches) > 1 { + // The first capture group contains the container ID + klog.V(4).Infof("Detected Podman container in user.slice: %s -> %s", cgroupPath, matches[1]) + return ContainerTypeLibpod, matches[1], nil + } + } case len(parts) < 2: return ContainerTypeStandaloneProcess, "", nil } diff --git a/containers/podman.go b/containers/podman.go new file mode 100644 index 0000000..8d9055a --- /dev/null +++ b/containers/podman.go @@ -0,0 +1,281 @@ +package containers + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/coroot/coroot-node-agent/common" + "github.com/coroot/coroot-node-agent/proc" + "github.com/coroot/logparser" + "inet.af/netaddr" + "k8s.io/klog/v2" +) + +const podmanTimeout = 30 * time.Second + +var ( + podmanClient *PodmanClient +) + +// PodmanClient represents a client for interacting with Podman API +type PodmanClient struct { + socketPath string +} + +// runCmd runs a command with a timeout and returns its output +func runCmd(command string, timeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, "sh", "-c", command) + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("command failed: %w, output: %s", err, string(output)) + } + return string(output), nil +} + +// isPodmanAvailable checks if the podman command is available +func isPodmanAvailable() bool { + _, err := exec.LookPath("podman") + return err == nil +} + +// PodmanInit initializes the Podman client +func PodmanInit() error { + klog.Infof("Initializing Podman support") + + // Try common Podman socket paths + sockets := []string{ + "/run/podman/podman.sock", + "/var/run/podman/podman.sock", + proc.HostPath("/run/podman/podman.sock"), + proc.HostPath("/var/run/podman/podman.sock"), + } + + for _, socket := range sockets { + // Check if the socket exists and is accessible + if _, err := os.Stat(socket); err == nil { + klog.Infof("Found Podman socket at %s", socket) + podmanClient = &PodmanClient{ + socketPath: socket, + } + break + } else { + klog.V(5).Infof("Podman socket not found at %s: %v", socket, err) + } + } + + // If no socket was found, that's okay - we'll try other approaches + if podmanClient == nil { + klog.Infof("No Podman socket found, will use CLI-based inspection") + } else { + klog.Infof("Using Podman socket at %s", podmanClient.socketPath) + } + + return nil +} + +// readContainerConfigFromFile reads container configuration from filesystem +func readContainerConfigFromFile(containerID string) (*ContainerMetadata, error) { + // Try to read container config from the standard Podman location + configPath := filepath.Join("/var/lib/containers/storage/overlay-containers", containerID, "userdata", "config.json") + configPath = proc.HostPath(configPath) + + if _, err := os.Stat(configPath); err != nil { + return nil, fmt.Errorf("config file not found: %w", err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + // Parse the container configuration + var config struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"rootfsImageName"` + Config struct { + Labels map[string]string `json:"Labels"` + Env []string `json:"Env"` + } `json:"config"` + Mounts []struct { + Source string `json:"source"` + Destination string `json:"destination"` + } `json:"mounts"` + NetworkSettings struct { + Ports map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } `json:"ports"` + } `json:"networkSettings"` + } + + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse container config: %w", err) + } + + res := &ContainerMetadata{ + name: strings.TrimPrefix(config.Name, "/"), + labels: config.Config.Labels, + image: config.Image, + volumes: map[string]string{}, + hostListens: map[string][]netaddr.IPPort{}, + networks: map[string]ContainerNetwork{}, + env: map[string]string{}, + } + + // Parse volumes + for _, mount := range config.Mounts { + res.volumes[mount.Destination] = common.ParseKubernetesVolumeSource(mount.Source) + } + + // Parse environment variables + for _, envVar := range config.Config.Env { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) == 2 { + res.env[parts[0]] = parts[1] + } + } + + // Parse network settings + for port, bindings := range config.NetworkSettings.Ports { + if len(bindings) > 0 { + ipport, err := netaddr.ParseIPPort(bindings[0].HostIP + ":" + bindings[0].HostPort) + if err != nil { + continue + } + res.hostListens[port] = append(res.hostListens[port], ipport) + } + } + + // Try to find log file + logPath := filepath.Join("/var/lib/containers/storage/overlay-containers", containerID, "userdata", "ctr.log") + logPath = proc.HostPath(logPath) + if _, err := os.Stat(logPath); err == nil { + res.logPath = logPath + res.logDecoder = logparser.DockerJsonDecoder{} // Podman uses same log format as Docker + } + + return res, nil +} + +// PodmanInspect inspects a container using multiple approaches +func PodmanInspect(containerID string) (*ContainerMetadata, error) { + klog.Infof("Inspecting Podman container %s", containerID) + + // First, try to read from filesystem directly + if md, err := readContainerConfigFromFile(containerID); err == nil { + klog.Infof("Successfully read container metadata from filesystem for %s", containerID) + return md, nil + } else { + klog.V(5).Infof("Failed to read container metadata from filesystem for %s: %v", containerID, err) + } + + // If filesystem read fails, try using podman command if available + if isPodmanAvailable() { + klog.Infof("Trying to get container metadata using podman command for %s", containerID) + // Run podman inspect command + cmd := fmt.Sprintf("podman inspect %s", containerID) + output, err := runCmd(cmd, podmanTimeout) + if err != nil { + klog.Warningf("Failed to run podman inspect for %s: %v", containerID, err) + return nil, fmt.Errorf("failed to run podman inspect: %w", err) + } + + // Parse the JSON output + var containers []struct { + ID string `json:"Id"` + Name string `json:"Name"` + Image string `json:"Image"` + Config struct { + Labels map[string]string `json:"Labels"` + Env []string `json:"Env"` + } `json:"Config"` + Mounts []struct { + Source string `json:"Source"` + Destination string `json:"Destination"` + } `json:"Mounts"` + NetworkSettings struct { + Ports map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } `json:"Ports"` + } `json:"NetworkSettings"` + } + + if err := json.Unmarshal([]byte(output), &containers); err != nil { + klog.Warningf("Failed to parse podman inspect output for %s: %v", containerID, err) + return nil, fmt.Errorf("failed to parse podman inspect output: %w", err) + } + + if len(containers) == 0 { + klog.Warningf("No container found with ID %s", containerID) + return nil, fmt.Errorf("no container found with ID %s", containerID) + } + + container := containers[0] + klog.Infof("Successfully inspected container %s with name %s", container.ID, container.Name) + + res := &ContainerMetadata{ + name: strings.TrimPrefix(container.Name, "/"), + labels: container.Config.Labels, + image: container.Image, + volumes: map[string]string{}, + hostListens: map[string][]netaddr.IPPort{}, + networks: map[string]ContainerNetwork{}, + env: map[string]string{}, + } + + // Parse volumes + for _, mount := range container.Mounts { + res.volumes[mount.Destination] = common.ParseKubernetesVolumeSource(mount.Source) + } + + // Parse environment variables + for _, envVar := range container.Config.Env { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) == 2 { + res.env[parts[0]] = parts[1] + } + } + + // Parse network settings + for port, bindings := range container.NetworkSettings.Ports { + if len(bindings) > 0 { + ipport, err := netaddr.ParseIPPort(bindings[0].HostIP + ":" + bindings[0].HostPort) + if err != nil { + continue + } + res.hostListens[port] = append(res.hostListens[port], ipport) + } + } + + // Try to find log file + logPath := fmt.Sprintf("/var/lib/containers/storage/overlay-containers/%s/userdata/ctr.log", containerID) + logPath = proc.HostPath(logPath) + if _, err := os.Stat(logPath); err == nil { + res.logPath = logPath + res.logDecoder = logparser.DockerJsonDecoder{} // Podman uses same log format as Docker + } + + return res, nil + } + + // If both approaches fail, return minimal metadata + klog.Warningf("Unable to get detailed metadata for Podman container %s, returning minimal metadata", containerID) + return &ContainerMetadata{ + name: fmt.Sprintf("libpod-%s", containerID[:12]), + image: "unknown", + labels: map[string]string{}, + volumes: map[string]string{}, + env: map[string]string{}, + }, nil +} \ No newline at end of file diff --git a/containers/registry.go b/containers/registry.go index a92225b..b17fcbd 100644 --- a/containers/registry.go +++ b/containers/registry.go @@ -101,6 +101,10 @@ func NewRegistry(reg prometheus.Registerer, processInfoCh chan<- ProcessInfo, gp if err = CrioInit(); err != nil { klog.Warningln(err) } + klog.Infoln("Initializing Podman support") + if err = PodmanInit(); err != nil { + klog.Warningln(err) + } if err = JournaldInit(); err != nil { klog.Warningln(err) } @@ -484,6 +488,12 @@ func calcId(cg *cgroup.Cgroup, md *ContainerMetadata) ContainerID { case cgroup.ContainerTypeTalosRuntime: return ContainerID(cg.ContainerId) case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio: + case cgroup.ContainerTypeLibpod: + // Handle Podman/libpod containers + if cg.ContainerId == "" { + return "" + } + return ContainerID("/libpod/" + cg.ContainerId) default: return "" } @@ -544,7 +554,7 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) { md := &ContainerMetadata{} md.systemdTriggeredBy = SystemdTriggeredBy(cg.ContainerId) return md, nil - case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio: + case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio, cgroup.ContainerTypeLibpod: default: return &ContainerMetadata{}, nil } @@ -554,6 +564,9 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) { if cg.ContainerType == cgroup.ContainerTypeCrio { return CrioInspect(cg.ContainerId) } + if cg.ContainerType == cgroup.ContainerTypeLibpod { + return PodmanInspect(cg.ContainerId) + } var dockerdErr error if dockerdClient != nil { md, err := DockerdInspect(cg.ContainerId)