Skip to content

Commit 5c15b2e

Browse files
author
Alexandru
committed
add Podman container support
Detect Podman/libpod containers via cgroup patterns: - rootful: machine.slice/libpod-<ID>.scope - rootless: user.slice/.../libpod-<ID>.scope - --cgroups=split: system.slice/<svc>/libpod-payload-<ID> - 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
1 parent e718e3c commit 5c15b2e

9 files changed

Lines changed: 322 additions & 15 deletions

File tree

cgroup/cgroup.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var (
2626
systemSliceIdRegexp = regexp.MustCompile(`(/(system|runtime|reserved|kube|azure)\.slice/([^/]+))`)
2727
talosIdRegexp = regexp.MustCompile(`/(system|podruntime)/([^/]+)`)
2828
lxcPayloadRegexp = regexp.MustCompile(`/lxc\.payload\.([^/]+)`)
29+
libpodIdRegexp = regexp.MustCompile(`libpod-([a-z0-9]{64})\.scope$`)
30+
libpodPayloadRegexp = regexp.MustCompile(`libpod-payload-([a-z0-9]{64})`)
2931
)
3032

3133
type ContainerType uint8
@@ -40,6 +42,7 @@ const (
4042
ContainerTypeSystemdService
4143
ContainerTypeSandbox
4244
ContainerTypeTalosRuntime
45+
ContainerTypePodman
4346
)
4447

4548
func (t ContainerType) String() string {
@@ -56,6 +59,8 @@ func (t ContainerType) String() string {
5659
return "lxc"
5760
case ContainerTypeSystemdService:
5861
return "systemd"
62+
case ContainerTypePodman:
63+
return "podman"
5964
default:
6065
return "unknown"
6166
}
@@ -162,7 +167,15 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) {
162167
switch {
163168
case cgroupPath == "/init":
164169
return ContainerTypeTalosRuntime, "/talos/init", nil
165-
case prefix == "user.slice" || prefix == "init.scope" || prefix == "systemd":
170+
case prefix == "user.slice":
171+
if strings.Contains(cgroupPath, "libpod-conmon-") {
172+
return ContainerTypeUnknown, "", nil
173+
}
174+
if matches := libpodIdRegexp.FindStringSubmatch(cgroupPath); matches != nil {
175+
return ContainerTypePodman, matches[1], nil
176+
}
177+
return ContainerTypeStandaloneProcess, "", nil
178+
case prefix == "init.scope" || prefix == "systemd":
166179
return ContainerTypeStandaloneProcess, "", nil
167180
case prefix == "docker" || (prefix == "system.slice" && len(parts) > 1 && strings.HasPrefix(parts[1], "docker-")):
168181
matches := dockerIdRegexp.FindStringSubmatch(cgroupPath)
@@ -194,6 +207,12 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) {
194207
}
195208
return ContainerTypeTalosRuntime, path.Join("/talos/", matches[2]), nil
196209
case prefix == "system.slice" || prefix == "runtime.slice" || prefix == "reserved.slice" || prefix == "kube.slice" || prefix == "azure.slice":
210+
if strings.Contains(cgroupPath, "libpod-conmon-") {
211+
return ContainerTypeUnknown, "", nil
212+
}
213+
if matches := libpodPayloadRegexp.FindStringSubmatch(cgroupPath); matches != nil {
214+
return ContainerTypePodman, matches[1], nil
215+
}
197216
if strings.HasSuffix(cgroupPath, ".scope") {
198217
return ContainerTypeStandaloneProcess, "", nil
199218
}
@@ -214,6 +233,14 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) {
214233
return ContainerTypeUnknown, "", fmt.Errorf("invalid lxc payload cgroup %s", cgroupPath)
215234
}
216235
return ContainerTypeLxc, "/lxc/" + matches[1], nil
236+
case prefix == "machine.slice":
237+
if strings.Contains(cgroupPath, "libpod-conmon-") {
238+
return ContainerTypeUnknown, "", nil
239+
}
240+
if matches := libpodIdRegexp.FindStringSubmatch(cgroupPath); matches != nil {
241+
return ContainerTypePodman, matches[1], nil
242+
}
243+
return ContainerTypeStandaloneProcess, "", nil
217244
case len(parts) < 2:
218245
return ContainerTypeStandaloneProcess, "", nil
219246
}

cgroup/cgroup_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,33 @@ func TestNewFromProcessCgroupFile(t *testing.T) {
8282
assert.Equal(t, "/lxc/first", cg.ContainerId)
8383
assert.Equal(t, ContainerTypeLxc, cg.ContainerType)
8484

85+
// Podman: rootful via machine.slice
86+
cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4000/cgroup"))
87+
require.Nil(t, err)
88+
assert.Equal(t, "/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope", cg.Id)
89+
assert.Equal(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", cg.ContainerId)
90+
assert.Equal(t, ContainerTypePodman, cg.ContainerType)
91+
92+
// Podman: rootless via user.slice
93+
cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4100/cgroup"))
94+
require.Nil(t, err)
95+
assert.Equal(t, "/user.slice/user-1000.slice/user@1000.service/libpod-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope", cg.Id)
96+
assert.Equal(t, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", cg.ContainerId)
97+
assert.Equal(t, ContainerTypePodman, cg.ContainerType)
98+
99+
// Podman: --cgroups=split via system.slice
100+
cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4200/cgroup"))
101+
require.Nil(t, err)
102+
assert.Equal(t, "/system.slice/myapp.service/libpod-payload-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", cg.Id)
103+
assert.Equal(t, "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", cg.ContainerId)
104+
assert.Equal(t, ContainerTypePodman, cg.ContainerType)
105+
106+
// Podman: conmon process (should be filtered)
107+
cg, err = NewFromProcessCgroupFile(path.Join("fixtures/proc/4300/cgroup"))
108+
require.Nil(t, err)
109+
assert.Equal(t, ContainerTypeUnknown, cg.ContainerType)
110+
assert.Equal(t, "", cg.ContainerId)
111+
85112
baseCgroupPath = "/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-podc83d0428_58af_41eb_8dba_b9e6eddffe7b.slice/docker-0e612005fd07e7f47e2cd07df99a2b4e909446814d71d0b5e4efc7159dd51252.scope"
86113
defer func() {
87114
baseCgroupPath = ""
@@ -225,4 +252,52 @@ func TestContainerByCgroup(t *testing.T) {
225252
as.Equal(ContainerTypeDocker, typ)
226253
as.Equal("ba7b10d15d16e10e3de7a2dcd408a3d971169ae303f46cfad4c5453c6326fee2", id)
227254
as.Nil(err)
255+
256+
// Podman: rootful via machine.slice
257+
typ, id, err = containerByCgroup("/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope")
258+
as.Equal(ContainerTypePodman, typ)
259+
as.Equal("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", id)
260+
as.Nil(err)
261+
262+
// Podman: rootful conmon (should be filtered)
263+
typ, id, err = containerByCgroup("/machine.slice/libpod-conmon-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope")
264+
as.Equal(ContainerTypeUnknown, typ)
265+
as.Equal("", id)
266+
as.Nil(err)
267+
268+
// Non-libpod machine.slice (e.g. QEMU VM)
269+
typ, id, err = containerByCgroup("/machine.slice/qemu-1-fedora.scope")
270+
as.Equal(ContainerTypeStandaloneProcess, typ)
271+
as.Equal("", id)
272+
as.Nil(err)
273+
274+
// Podman: rootless via user.slice
275+
typ, id, err = containerByCgroup("/user.slice/user-1000.slice/user@1000.service/libpod-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.scope")
276+
as.Equal(ContainerTypePodman, typ)
277+
as.Equal("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", id)
278+
as.Nil(err)
279+
280+
// Podman: rootless conmon (should be filtered)
281+
typ, id, err = containerByCgroup("/user.slice/user-1000.slice/user@1000.service/libpod-conmon-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.scope")
282+
as.Equal(ContainerTypeUnknown, typ)
283+
as.Equal("", id)
284+
as.Nil(err)
285+
286+
// Non-libpod user.slice (existing behavior preserved)
287+
typ, id, err = containerByCgroup("/user.slice/user-1000.slice/session-1.scope")
288+
as.Equal(ContainerTypeStandaloneProcess, typ)
289+
as.Equal("", id)
290+
as.Nil(err)
291+
292+
// Podman: --cgroups=split via system.slice (apollo13 pattern)
293+
typ, id, err = containerByCgroup("/system.slice/myapp.service/libpod-payload-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee")
294+
as.Equal(ContainerTypePodman, typ)
295+
as.Equal("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", id)
296+
as.Nil(err)
297+
298+
// Podman: --cgroups=split conmon (should be filtered)
299+
typ, id, err = containerByCgroup("/system.slice/myapp.service/libpod-conmon-ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
300+
as.Equal(ContainerTypeUnknown, typ)
301+
as.Equal("", id)
302+
as.Nil(err)
228303
}

cgroup/fixtures/proc/4000/cgroup

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0::/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope

cgroup/fixtures/proc/4100/cgroup

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0::/user.slice/user-1000.slice/user@1000.service/libpod-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope

cgroup/fixtures/proc/4200/cgroup

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0::/system.slice/myapp.service/libpod-payload-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc

cgroup/fixtures/proc/4300/cgroup

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0::/machine.slice/libpod-conmon-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.scope

containers/container.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ type ContainerNetwork struct {
3939
}
4040

4141
type ContainerMetadata struct {
42-
name string
43-
labels map[string]string
44-
volumes map[string]string
45-
logPath string
46-
image string
47-
logDecoder logparser.Decoder
48-
hostListens map[string][]netaddr.IPPort
49-
networks map[string]ContainerNetwork
50-
env map[string]string
51-
systemd SystemdProperties
42+
name string
43+
labels map[string]string
44+
volumes map[string]string
45+
logPath string
46+
image string
47+
logDecoder logparser.Decoder
48+
hostListens map[string][]netaddr.IPPort
49+
networks map[string]ContainerNetwork
50+
env map[string]string
51+
systemd SystemdProperties
52+
podmanJournaldUnit string
5253
}
5354

5455
type Delays struct {
@@ -1100,6 +1101,37 @@ func (c *Container) runLogParser(logPath string) {
11001101
}
11011102
klog.InfoS("started container logparser", "cg", c.cgroup.Id)
11021103
c.logParsers["stdout/stderr"] = &LogParser{parser: parser, stop: reader.Stop}
1104+
1105+
case cgroup.ContainerTypePodman:
1106+
if c.metadata.logPath != "" {
1107+
if c.logParsers["stdout/stderr"] != nil {
1108+
return
1109+
}
1110+
ch := make(chan logparser.LogEntry)
1111+
parser := logparser.NewParser(ch, c.metadata.logDecoder, logs.OtelLogEmitter(containerId), multilineCollectorTimeout, *flags.LogPatternsPerContainer)
1112+
reader, err := logs.NewTailReader(proc.HostPath(c.metadata.logPath), ch)
1113+
if err != nil {
1114+
klog.Warningln(err)
1115+
parser.Stop()
1116+
return
1117+
}
1118+
klog.InfoS("started podman logparser", "cg", c.cgroup.Id)
1119+
c.logParsers["stdout/stderr"] = &LogParser{parser: parser, stop: reader.Stop}
1120+
return
1121+
}
1122+
unit := c.metadata.podmanJournaldUnit
1123+
if unit == "" {
1124+
unit = "libpod-" + c.cgroup.ContainerId + ".scope"
1125+
}
1126+
ch := make(chan logparser.LogEntry)
1127+
if err := JournaldSubscribe(unit, ch); err != nil {
1128+
klog.Warningln(err)
1129+
return
1130+
}
1131+
parser := logparser.NewParser(ch, nil, logs.OtelLogEmitter(containerId), multilineCollectorTimeout, *flags.LogPatternsPerContainer)
1132+
stop := func() { JournaldUnsubscribe(unit) }
1133+
klog.InfoS("started podman journald logparser", "cg", c.cgroup.Id, "unit", unit)
1134+
c.logParsers["journald"] = &LogParser{parser: parser, stop: stop}
11031135
}
11041136
}
11051137

containers/podman.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package containers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net"
9+
"net/http"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
"github.com/coroot/coroot-node-agent/common"
15+
"github.com/coroot/coroot-node-agent/proc"
16+
"github.com/coroot/logparser"
17+
"inet.af/netaddr"
18+
"k8s.io/klog/v2"
19+
)
20+
21+
const podmanTimeout = 30 * time.Second
22+
23+
var podmanClient *http.Client
24+
25+
func PodmanInit() error {
26+
sockets := []string{
27+
"/run/podman/podman.sock",
28+
"/var/run/podman/podman.sock",
29+
}
30+
var podmanSocket string
31+
for _, socket := range sockets {
32+
socketHostPath := proc.HostPath(socket)
33+
if _, err := os.Stat(socketHostPath); err == nil {
34+
podmanSocket = socketHostPath
35+
break
36+
}
37+
}
38+
if podmanSocket == "" {
39+
return fmt.Errorf("podman socket not found in [%s]", strings.Join(sockets, ","))
40+
}
41+
klog.Infoln("podman socket:", podmanSocket)
42+
43+
podmanClient = &http.Client{
44+
Transport: &http.Transport{
45+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
46+
return net.DialTimeout("unix", podmanSocket, podmanTimeout)
47+
},
48+
DisableCompression: true,
49+
},
50+
}
51+
return nil
52+
}
53+
54+
type podmanContainerInfo struct {
55+
Name string `json:"Name"`
56+
Image string `json:"ImageName"`
57+
Config struct {
58+
Labels map[string]string `json:"Labels"`
59+
Env []string `json:"Env"`
60+
} `json:"Config"`
61+
Mounts []struct {
62+
Source string `json:"Source"`
63+
Destination string `json:"Destination"`
64+
} `json:"Mounts"`
65+
HostConfig struct {
66+
LogConfig struct {
67+
Type string `json:"Type"`
68+
} `json:"LogConfig"`
69+
} `json:"HostConfig"`
70+
NetworkSettings struct {
71+
Ports map[string][]struct {
72+
HostIP string `json:"HostIp"`
73+
HostPort string `json:"HostPort"`
74+
} `json:"Ports"`
75+
} `json:"NetworkSettings"`
76+
LogPath string `json:"LogPath"`
77+
}
78+
79+
func PodmanInspect(containerID string) (*ContainerMetadata, error) {
80+
if podmanClient == nil {
81+
return nil, fmt.Errorf("podman client not initialized")
82+
}
83+
resp, err := podmanClient.Get("http://localhost/v4.0.0/libpod/containers/" + containerID + "/json")
84+
if err != nil {
85+
return nil, err
86+
}
87+
defer resp.Body.Close()
88+
89+
if resp.StatusCode != http.StatusOK {
90+
return nil, errors.New(resp.Status)
91+
}
92+
93+
i := &podmanContainerInfo{}
94+
if err = json.NewDecoder(resp.Body).Decode(i); err != nil {
95+
return nil, err
96+
}
97+
98+
res := &ContainerMetadata{
99+
name: strings.TrimPrefix(i.Name, "/"),
100+
image: i.Image,
101+
labels: i.Config.Labels,
102+
volumes: map[string]string{},
103+
hostListens: map[string][]netaddr.IPPort{},
104+
networks: map[string]ContainerNetwork{},
105+
env: map[string]string{},
106+
}
107+
if res.labels == nil {
108+
res.labels = map[string]string{}
109+
}
110+
111+
for _, m := range i.Mounts {
112+
res.volumes[m.Destination] = common.ParseKubernetesVolumeSource(m.Source)
113+
}
114+
115+
for _, value := range i.Config.Env {
116+
idx := strings.Index(value, "=")
117+
if idx < 0 {
118+
continue
119+
}
120+
res.env[value[:idx]] = value[idx+1:]
121+
}
122+
123+
if i.NetworkSettings.Ports != nil {
124+
addrs := map[netaddr.IPPort]struct{}{}
125+
for port, bindings := range i.NetworkSettings.Ports {
126+
if !strings.HasSuffix(port, "/tcp") {
127+
continue
128+
}
129+
for _, b := range bindings {
130+
if ipp, err := netaddr.ParseIPPort(b.HostIP + ":" + b.HostPort); err == nil {
131+
addrs[ipp] = struct{}{}
132+
}
133+
}
134+
}
135+
if len(addrs) > 0 {
136+
s := make([]netaddr.IPPort, 0, len(addrs))
137+
for addr := range addrs {
138+
if common.PortFilter.ShouldBeSkipped(addr.Port()) {
139+
continue
140+
}
141+
s = append(s, addr)
142+
}
143+
res.hostListens["podman"] = s
144+
}
145+
}
146+
147+
switch i.HostConfig.LogConfig.Type {
148+
case "json-file", "k8s-file":
149+
if i.LogPath != "" {
150+
res.logPath = i.LogPath
151+
res.logDecoder = logparser.DockerJsonDecoder{}
152+
}
153+
default:
154+
// journald is the Podman default log driver.
155+
// Store the unit name so runLogParser can subscribe via journald.
156+
res.podmanJournaldUnit = "libpod-" + containerID + ".scope"
157+
}
158+
159+
return res, nil
160+
}

0 commit comments

Comments
 (0)