From 5c15b2ee7ccec35defff612a42314ecbd47dda70 Mon Sep 17 00:00:00 2001 From: Alexandru Date: Fri, 27 Feb 2026 15:28:13 +0200 Subject: [PATCH] add Podman container support Detect Podman/libpod containers via cgroup patterns: - rootful: machine.slice/libpod-.scope - rootless: user.slice/.../libpod-.scope - --cgroups=split: system.slice//libpod-payload- - conmon processes are filtered (like crio-conmon) Use Podman REST API via Unix socket for container inspection, following the same http.Client pattern as CRI-O. Log collection supports both journald (Podman default) and json-file log drivers via the existing journald reader and file tail reader respectively. Authored by LastCoder | e6d8e0a6-31aa-49de-8456-dc6577890436 --- cgroup/cgroup.go | 29 +++++- cgroup/cgroup_test.go | 75 +++++++++++++++ cgroup/fixtures/proc/4000/cgroup | 1 + cgroup/fixtures/proc/4100/cgroup | 1 + cgroup/fixtures/proc/4200/cgroup | 1 + cgroup/fixtures/proc/4300/cgroup | 1 + containers/container.go | 52 ++++++++-- containers/podman.go | 160 +++++++++++++++++++++++++++++++ containers/registry.go | 17 +++- 9 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 cgroup/fixtures/proc/4000/cgroup create mode 100644 cgroup/fixtures/proc/4100/cgroup create mode 100644 cgroup/fixtures/proc/4200/cgroup create mode 100644 cgroup/fixtures/proc/4300/cgroup create mode 100644 containers/podman.go diff --git a/cgroup/cgroup.go b/cgroup/cgroup.go index 04c95c8..136f0a2 100644 --- a/cgroup/cgroup.go +++ b/cgroup/cgroup.go @@ -26,6 +26,8 @@ var ( systemSliceIdRegexp = regexp.MustCompile(`(/(system|runtime|reserved|kube|azure)\.slice/([^/]+))`) talosIdRegexp = regexp.MustCompile(`/(system|podruntime)/([^/]+)`) lxcPayloadRegexp = regexp.MustCompile(`/lxc\.payload\.([^/]+)`) + libpodIdRegexp = regexp.MustCompile(`libpod-([a-z0-9]{64})\.scope$`) + libpodPayloadRegexp = regexp.MustCompile(`libpod-payload-([a-z0-9]{64})`) ) type ContainerType uint8 @@ -40,6 +42,7 @@ const ( ContainerTypeSystemdService ContainerTypeSandbox ContainerTypeTalosRuntime + ContainerTypePodman ) func (t ContainerType) String() string { @@ -56,6 +59,8 @@ func (t ContainerType) String() string { return "lxc" case ContainerTypeSystemdService: return "systemd" + case ContainerTypePodman: + return "podman" default: return "unknown" } @@ -162,7 +167,15 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) { switch { case cgroupPath == "/init": return ContainerTypeTalosRuntime, "/talos/init", nil - case prefix == "user.slice" || prefix == "init.scope" || prefix == "systemd": + case prefix == "user.slice": + if strings.Contains(cgroupPath, "libpod-conmon-") { + return ContainerTypeUnknown, "", nil + } + if matches := libpodIdRegexp.FindStringSubmatch(cgroupPath); matches != nil { + return ContainerTypePodman, matches[1], nil + } + return ContainerTypeStandaloneProcess, "", nil + case prefix == "init.scope" || prefix == "systemd": return ContainerTypeStandaloneProcess, "", nil case prefix == "docker" || (prefix == "system.slice" && len(parts) > 1 && strings.HasPrefix(parts[1], "docker-")): matches := dockerIdRegexp.FindStringSubmatch(cgroupPath) @@ -194,6 +207,12 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) { } return ContainerTypeTalosRuntime, path.Join("/talos/", matches[2]), nil case prefix == "system.slice" || prefix == "runtime.slice" || prefix == "reserved.slice" || prefix == "kube.slice" || prefix == "azure.slice": + if strings.Contains(cgroupPath, "libpod-conmon-") { + return ContainerTypeUnknown, "", nil + } + if matches := libpodPayloadRegexp.FindStringSubmatch(cgroupPath); matches != nil { + return ContainerTypePodman, matches[1], nil + } if strings.HasSuffix(cgroupPath, ".scope") { return ContainerTypeStandaloneProcess, "", nil } @@ -214,6 +233,14 @@ 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": + if strings.Contains(cgroupPath, "libpod-conmon-") { + return ContainerTypeUnknown, "", nil + } + if matches := libpodIdRegexp.FindStringSubmatch(cgroupPath); matches != nil { + return ContainerTypePodman, matches[1], nil + } + return ContainerTypeStandaloneProcess, "", nil case len(parts) < 2: return ContainerTypeStandaloneProcess, "", nil } diff --git a/cgroup/cgroup_test.go b/cgroup/cgroup_test.go index 7e8b7af..b883f0d 100644 --- a/cgroup/cgroup_test.go +++ b/cgroup/cgroup_test.go @@ -82,6 +82,33 @@ func TestNewFromProcessCgroupFile(t *testing.T) { assert.Equal(t, "/lxc/first", cg.ContainerId) assert.Equal(t, ContainerTypeLxc, cg.ContainerType) + // Podman: rootful via machine.slice + cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4000/cgroup")) + require.Nil(t, err) + assert.Equal(t, "/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope", cg.Id) + assert.Equal(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", cg.ContainerId) + assert.Equal(t, ContainerTypePodman, cg.ContainerType) + + // Podman: rootless via user.slice + cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4100/cgroup")) + require.Nil(t, err) + assert.Equal(t, "/user.slice/user-1000.slice/user@1000.service/libpod-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope", cg.Id) + assert.Equal(t, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", cg.ContainerId) + assert.Equal(t, ContainerTypePodman, cg.ContainerType) + + // Podman: --cgroups=split via system.slice + cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4200/cgroup")) + require.Nil(t, err) + assert.Equal(t, "/system.slice/myapp.service/libpod-payload-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", cg.Id) + assert.Equal(t, "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", cg.ContainerId) + assert.Equal(t, ContainerTypePodman, cg.ContainerType) + + // Podman: conmon process (should be filtered) + cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4300/cgroup")) + require.Nil(t, err) + assert.Equal(t, ContainerTypeUnknown, cg.ContainerType) + assert.Equal(t, "", cg.ContainerId) + baseCgroupPath = "/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-podc83d0428_58af_41eb_8dba_b9e6eddffe7b.slice/docker-0e612005fd07e7f47e2cd07df99a2b4e909446814d71d0b5e4efc7159dd51252.scope" defer func() { baseCgroupPath = "" @@ -225,4 +252,52 @@ func TestContainerByCgroup(t *testing.T) { as.Equal(ContainerTypeDocker, typ) as.Equal("ba7b10d15d16e10e3de7a2dcd408a3d971169ae303f46cfad4c5453c6326fee2", id) as.Nil(err) + + // Podman: rootful via machine.slice + typ, id, err = containerByCgroup("/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope") + as.Equal(ContainerTypePodman, typ) + as.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", id) + as.Nil(err) + + // Podman: rootful conmon (should be filtered) + typ, id, err = containerByCgroup("/machine.slice/libpod-conmon-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope") + as.Equal(ContainerTypeUnknown, typ) + as.Equal("", id) + as.Nil(err) + + // Non-libpod machine.slice (e.g. QEMU VM) + typ, id, err = containerByCgroup("/machine.slice/qemu-1-fedora.scope") + as.Equal(ContainerTypeStandaloneProcess, typ) + as.Equal("", id) + as.Nil(err) + + // Podman: rootless via user.slice + typ, id, err = containerByCgroup("/user.slice/user-1000.slice/user@1000.service/libpod-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.scope") + as.Equal(ContainerTypePodman, typ) + as.Equal("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", id) + as.Nil(err) + + // Podman: rootless conmon (should be filtered) + typ, id, err = containerByCgroup("/user.slice/user-1000.slice/user@1000.service/libpod-conmon-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.scope") + as.Equal(ContainerTypeUnknown, typ) + as.Equal("", id) + as.Nil(err) + + // Non-libpod user.slice (existing behavior preserved) + typ, id, err = containerByCgroup("/user.slice/user-1000.slice/session-1.scope") + as.Equal(ContainerTypeStandaloneProcess, typ) + as.Equal("", id) + as.Nil(err) + + // Podman: --cgroups=split via system.slice (apollo13 pattern) + typ, id, err = containerByCgroup("/system.slice/myapp.service/libpod-payload-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + as.Equal(ContainerTypePodman, typ) + as.Equal("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", id) + as.Nil(err) + + // Podman: --cgroups=split conmon (should be filtered) + typ, id, err = containerByCgroup("/system.slice/myapp.service/libpod-conmon-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + as.Equal(ContainerTypeUnknown, typ) + as.Equal("", id) + as.Nil(err) } diff --git a/cgroup/fixtures/proc/4000/cgroup b/cgroup/fixtures/proc/4000/cgroup new file mode 100644 index 0000000..5b70151 --- /dev/null +++ b/cgroup/fixtures/proc/4000/cgroup @@ -0,0 +1 @@ +0::/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope diff --git a/cgroup/fixtures/proc/4100/cgroup b/cgroup/fixtures/proc/4100/cgroup new file mode 100644 index 0000000..46fcd6d --- /dev/null +++ b/cgroup/fixtures/proc/4100/cgroup @@ -0,0 +1 @@ +0::/user.slice/user-1000.slice/user@1000.service/libpod-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope diff --git a/cgroup/fixtures/proc/4200/cgroup b/cgroup/fixtures/proc/4200/cgroup new file mode 100644 index 0000000..5039218 --- /dev/null +++ b/cgroup/fixtures/proc/4200/cgroup @@ -0,0 +1 @@ +0::/system.slice/myapp.service/libpod-payload-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc diff --git a/cgroup/fixtures/proc/4300/cgroup b/cgroup/fixtures/proc/4300/cgroup new file mode 100644 index 0000000..2b159e8 --- /dev/null +++ b/cgroup/fixtures/proc/4300/cgroup @@ -0,0 +1 @@ +0::/machine.slice/libpod-conmon-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.scope diff --git a/containers/container.go b/containers/container.go index abb2cfb..5c7e91b 100644 --- a/containers/container.go +++ b/containers/container.go @@ -39,16 +39,17 @@ type ContainerNetwork struct { } type ContainerMetadata struct { - name string - labels map[string]string - volumes map[string]string - logPath string - image string - logDecoder logparser.Decoder - hostListens map[string][]netaddr.IPPort - networks map[string]ContainerNetwork - env map[string]string - systemd SystemdProperties + name string + labels map[string]string + volumes map[string]string + logPath string + image string + logDecoder logparser.Decoder + hostListens map[string][]netaddr.IPPort + networks map[string]ContainerNetwork + env map[string]string + systemd SystemdProperties + podmanJournaldUnit string } type Delays struct { @@ -1100,6 +1101,37 @@ func (c *Container) runLogParser(logPath string) { } klog.InfoS("started container logparser", "cg", c.cgroup.Id) c.logParsers["stdout/stderr"] = &LogParser{parser: parser, stop: reader.Stop} + + case cgroup.ContainerTypePodman: + if c.metadata.logPath != "" { + if c.logParsers["stdout/stderr"] != nil { + return + } + ch := make(chan logparser.LogEntry) + parser := logparser.NewParser(ch, c.metadata.logDecoder, logs.OtelLogEmitter(containerId), multilineCollectorTimeout, *flags.LogPatternsPerContainer) + reader, err := logs.NewTailReader(proc.HostPath(c.metadata.logPath), ch) + if err != nil { + klog.Warningln(err) + parser.Stop() + return + } + klog.InfoS("started podman logparser", "cg", c.cgroup.Id) + c.logParsers["stdout/stderr"] = &LogParser{parser: parser, stop: reader.Stop} + return + } + unit := c.metadata.podmanJournaldUnit + if unit == "" { + unit = "libpod-" + c.cgroup.ContainerId + ".scope" + } + ch := make(chan logparser.LogEntry) + if err := JournaldSubscribe(unit, ch); err != nil { + klog.Warningln(err) + return + } + parser := logparser.NewParser(ch, nil, logs.OtelLogEmitter(containerId), multilineCollectorTimeout, *flags.LogPatternsPerContainer) + stop := func() { JournaldUnsubscribe(unit) } + klog.InfoS("started podman journald logparser", "cg", c.cgroup.Id, "unit", unit) + c.logParsers["journald"] = &LogParser{parser: parser, stop: stop} } } diff --git a/containers/podman.go b/containers/podman.go new file mode 100644 index 0000000..c1640d2 --- /dev/null +++ b/containers/podman.go @@ -0,0 +1,160 @@ +package containers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "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 *http.Client + +func PodmanInit() error { + sockets := []string{ + "/run/podman/podman.sock", + "/var/run/podman/podman.sock", + } + var podmanSocket string + for _, socket := range sockets { + socketHostPath := proc.HostPath(socket) + if _, err := os.Stat(socketHostPath); err == nil { + podmanSocket = socketHostPath + break + } + } + if podmanSocket == "" { + return fmt.Errorf("podman socket not found in [%s]", strings.Join(sockets, ",")) + } + klog.Infoln("podman socket:", podmanSocket) + + podmanClient = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.DialTimeout("unix", podmanSocket, podmanTimeout) + }, + DisableCompression: true, + }, + } + return nil +} + +type podmanContainerInfo struct { + Name string `json:"Name"` + Image string `json:"ImageName"` + 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"` + HostConfig struct { + LogConfig struct { + Type string `json:"Type"` + } `json:"LogConfig"` + } `json:"HostConfig"` + NetworkSettings struct { + Ports map[string][]struct { + HostIP string `json:"HostIp"` + HostPort string `json:"HostPort"` + } `json:"Ports"` + } `json:"NetworkSettings"` + LogPath string `json:"LogPath"` +} + +func PodmanInspect(containerID string) (*ContainerMetadata, error) { + if podmanClient == nil { + return nil, fmt.Errorf("podman client not initialized") + } + resp, err := podmanClient.Get("http://localhost/v4.0.0/libpod/containers/" + containerID + "/json") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + + i := &podmanContainerInfo{} + if err = json.NewDecoder(resp.Body).Decode(i); err != nil { + return nil, err + } + + res := &ContainerMetadata{ + name: strings.TrimPrefix(i.Name, "/"), + image: i.Image, + labels: i.Config.Labels, + volumes: map[string]string{}, + hostListens: map[string][]netaddr.IPPort{}, + networks: map[string]ContainerNetwork{}, + env: map[string]string{}, + } + if res.labels == nil { + res.labels = map[string]string{} + } + + for _, m := range i.Mounts { + res.volumes[m.Destination] = common.ParseKubernetesVolumeSource(m.Source) + } + + for _, value := range i.Config.Env { + idx := strings.Index(value, "=") + if idx < 0 { + continue + } + res.env[value[:idx]] = value[idx+1:] + } + + if i.NetworkSettings.Ports != nil { + addrs := map[netaddr.IPPort]struct{}{} + for port, bindings := range i.NetworkSettings.Ports { + if !strings.HasSuffix(port, "/tcp") { + continue + } + for _, b := range bindings { + if ipp, err := netaddr.ParseIPPort(b.HostIP + ":" + b.HostPort); err == nil { + addrs[ipp] = struct{}{} + } + } + } + if len(addrs) > 0 { + s := make([]netaddr.IPPort, 0, len(addrs)) + for addr := range addrs { + if common.PortFilter.ShouldBeSkipped(addr.Port()) { + continue + } + s = append(s, addr) + } + res.hostListens["podman"] = s + } + } + + switch i.HostConfig.LogConfig.Type { + case "json-file", "k8s-file": + if i.LogPath != "" { + res.logPath = i.LogPath + res.logDecoder = logparser.DockerJsonDecoder{} + } + default: + // journald is the Podman default log driver. + // Store the unit name so runLogParser can subscribe via journald. + res.podmanJournaldUnit = "libpod-" + containerID + ".scope" + } + + return res, nil +} diff --git a/containers/registry.go b/containers/registry.go index b5f6a00..9332e0c 100644 --- a/containers/registry.go +++ b/containers/registry.go @@ -101,6 +101,9 @@ func NewRegistry(reg prometheus.Registerer, processInfoCh chan<- ProcessInfo, gp if err = CrioInit(); err != nil { klog.Warningln(err) } + if err = PodmanInit(); err != nil { + klog.Warningln(err) + } if err = JournaldInit(); err != nil { klog.Warningln(err) } @@ -490,7 +493,7 @@ func calcId(cg *cgroup.Cgroup, md *ContainerMetadata) ContainerID { return ContainerID(cg.ContainerId) case cgroup.ContainerTypeTalosRuntime: return ContainerID(cg.ContainerId) - case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio: + case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio, cgroup.ContainerTypePodman: default: return "" } @@ -538,10 +541,13 @@ func calcId(cg *cgroup.Cgroup, md *ContainerMetadata) ContainerID { return ContainerID(fmt.Sprintf("/nomad/%s/%s/%s/%s/%s", namespace, job, group, allocId, task)) } } - if md.name == "" { // should be "pure" dockerd container here - klog.Warningln("empty dockerd container name for:", cg.ContainerId) + if md.name == "" { + klog.Warningln("empty container name for:", cg.ContainerId) return "" } + if cg.ContainerType == cgroup.ContainerTypePodman { + return ContainerID("/podman/" + md.name) + } return ContainerID("/docker/" + md.name) } @@ -552,7 +558,7 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) { md := &ContainerMetadata{} md.systemd, err = getSystemdProperties(cg.Id) return md, err - case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio: + case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio, cgroup.ContainerTypePodman: default: return &ContainerMetadata{}, nil } @@ -562,6 +568,9 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) { if cg.ContainerType == cgroup.ContainerTypeCrio { return CrioInspect(cg.ContainerId) } + if cg.ContainerType == cgroup.ContainerTypePodman { + return PodmanInspect(cg.ContainerId) + } var dockerdErr error if dockerdClient != nil { md, err := DockerdInspect(cg.ContainerId)