Skip to content

Commit e9a638f

Browse files
author
Alexandru Vizitiu
committed
Add support for Podman
1 parent eab45e1 commit e9a638f

3 files changed

Lines changed: 323 additions & 1 deletion

File tree

cgroup/cgroup.go

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

3133
type ContainerType uint8
@@ -40,6 +42,7 @@ const (
4042
ContainerTypeSystemdService
4143
ContainerTypeSandbox
4244
ContainerTypeTalosRuntime
45+
ContainerTypeLibpod
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 ContainerTypeLibpod:
63+
return "libpod"
5964
default:
6065
return "unknown"
6166
}
@@ -211,6 +216,29 @@ func containerByCgroup(cgroupPath string) (ContainerType, string, error) {
211216
return ContainerTypeUnknown, "", fmt.Errorf("invalid lxc payload cgroup %s", cgroupPath)
212217
}
213218
return ContainerTypeLxc, "/lxc/" + matches[1], nil
219+
case prefix == "machine.slice":
220+
// Handle Podman/libpod containers
221+
// Pattern: /machine.slice/libpod-<ID>.scope or /machine.slice/libpod-conmon-<ID>.scope
222+
if strings.Contains(cgroupPath, "libpod-") {
223+
// Extract the libpod container ID
224+
matches := libpodIdRegexp.FindStringSubmatch(cgroupPath)
225+
if matches != nil && len(matches) > 1 {
226+
// The first capture group contains the container ID
227+
klog.V(4).Infof("Detected Podman container in machine.slice: %s -> %s", cgroupPath, matches[1])
228+
return ContainerTypeLibpod, matches[1], nil
229+
}
230+
}
231+
case prefix == "user.slice":
232+
// Check if this might be a Podman container running in user session
233+
// Look for libpod patterns in user slices as well
234+
if strings.Contains(cgroupPath, "libpod-") {
235+
matches := libpodIdRegexp.FindStringSubmatch(cgroupPath)
236+
if matches != nil && len(matches) > 1 {
237+
// The first capture group contains the container ID
238+
klog.V(4).Infof("Detected Podman container in user.slice: %s -> %s", cgroupPath, matches[1])
239+
return ContainerTypeLibpod, matches[1], nil
240+
}
241+
}
214242
case len(parts) < 2:
215243
return ContainerTypeStandaloneProcess, "", nil
216244
}

containers/podman.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
package containers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
"time"
12+
13+
"github.com/coroot/coroot-node-agent/common"
14+
"github.com/coroot/coroot-node-agent/proc"
15+
"github.com/coroot/logparser"
16+
"inet.af/netaddr"
17+
"k8s.io/klog/v2"
18+
)
19+
20+
const podmanTimeout = 30 * time.Second
21+
22+
var (
23+
podmanClient *PodmanClient
24+
)
25+
26+
// PodmanClient represents a client for interacting with Podman API
27+
type PodmanClient struct {
28+
socketPath string
29+
}
30+
31+
// runCmd runs a command with a timeout and returns its output
32+
func runCmd(command string, timeout time.Duration) (string, error) {
33+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
34+
defer cancel()
35+
36+
cmd := exec.CommandContext(ctx, "sh", "-c", command)
37+
output, err := cmd.CombinedOutput()
38+
if err != nil {
39+
return "", fmt.Errorf("command failed: %w, output: %s", err, string(output))
40+
}
41+
return string(output), nil
42+
}
43+
44+
// isPodmanAvailable checks if the podman command is available
45+
func isPodmanAvailable() bool {
46+
_, err := exec.LookPath("podman")
47+
return err == nil
48+
}
49+
50+
// PodmanInit initializes the Podman client
51+
func PodmanInit() error {
52+
klog.Infof("Initializing Podman support")
53+
54+
// Try common Podman socket paths
55+
sockets := []string{
56+
"/run/podman/podman.sock",
57+
"/var/run/podman/podman.sock",
58+
proc.HostPath("/run/podman/podman.sock"),
59+
proc.HostPath("/var/run/podman/podman.sock"),
60+
}
61+
62+
for _, socket := range sockets {
63+
// Check if the socket exists and is accessible
64+
if _, err := os.Stat(socket); err == nil {
65+
klog.Infof("Found Podman socket at %s", socket)
66+
podmanClient = &PodmanClient{
67+
socketPath: socket,
68+
}
69+
break
70+
} else {
71+
klog.V(5).Infof("Podman socket not found at %s: %v", socket, err)
72+
}
73+
}
74+
75+
// If no socket was found, that's okay - we'll try other approaches
76+
if podmanClient == nil {
77+
klog.Infof("No Podman socket found, will use CLI-based inspection")
78+
} else {
79+
klog.Infof("Using Podman socket at %s", podmanClient.socketPath)
80+
}
81+
82+
return nil
83+
}
84+
85+
// readContainerConfigFromFile reads container configuration from filesystem
86+
func readContainerConfigFromFile(containerID string) (*ContainerMetadata, error) {
87+
// Try to read container config from the standard Podman location
88+
configPath := filepath.Join("/var/lib/containers/storage/overlay-containers", containerID, "userdata", "config.json")
89+
configPath = proc.HostPath(configPath)
90+
91+
if _, err := os.Stat(configPath); err != nil {
92+
return nil, fmt.Errorf("config file not found: %w", err)
93+
}
94+
95+
data, err := os.ReadFile(configPath)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to read config file: %w", err)
98+
}
99+
100+
// Parse the container configuration
101+
var config struct {
102+
ID string `json:"id"`
103+
Name string `json:"name"`
104+
Image string `json:"rootfsImageName"`
105+
Config struct {
106+
Labels map[string]string `json:"Labels"`
107+
Env []string `json:"Env"`
108+
} `json:"config"`
109+
Mounts []struct {
110+
Source string `json:"source"`
111+
Destination string `json:"destination"`
112+
} `json:"mounts"`
113+
NetworkSettings struct {
114+
Ports map[string][]struct {
115+
HostIP string `json:"HostIp"`
116+
HostPort string `json:"HostPort"`
117+
} `json:"ports"`
118+
} `json:"networkSettings"`
119+
}
120+
121+
if err := json.Unmarshal(data, &config); err != nil {
122+
return nil, fmt.Errorf("failed to parse container config: %w", err)
123+
}
124+
125+
res := &ContainerMetadata{
126+
name: strings.TrimPrefix(config.Name, "/"),
127+
labels: config.Config.Labels,
128+
image: config.Image,
129+
volumes: map[string]string{},
130+
hostListens: map[string][]netaddr.IPPort{},
131+
networks: map[string]ContainerNetwork{},
132+
env: map[string]string{},
133+
}
134+
135+
// Parse volumes
136+
for _, mount := range config.Mounts {
137+
res.volumes[mount.Destination] = common.ParseKubernetesVolumeSource(mount.Source)
138+
}
139+
140+
// Parse environment variables
141+
for _, envVar := range config.Config.Env {
142+
parts := strings.SplitN(envVar, "=", 2)
143+
if len(parts) == 2 {
144+
res.env[parts[0]] = parts[1]
145+
}
146+
}
147+
148+
// Parse network settings
149+
for port, bindings := range config.NetworkSettings.Ports {
150+
if len(bindings) > 0 {
151+
ipport, err := netaddr.ParseIPPort(bindings[0].HostIP + ":" + bindings[0].HostPort)
152+
if err != nil {
153+
continue
154+
}
155+
res.hostListens[port] = append(res.hostListens[port], ipport)
156+
}
157+
}
158+
159+
// Try to find log file
160+
logPath := filepath.Join("/var/lib/containers/storage/overlay-containers", containerID, "userdata", "ctr.log")
161+
logPath = proc.HostPath(logPath)
162+
if _, err := os.Stat(logPath); err == nil {
163+
res.logPath = logPath
164+
res.logDecoder = logparser.DockerJsonDecoder{} // Podman uses same log format as Docker
165+
}
166+
167+
return res, nil
168+
}
169+
170+
// PodmanInspect inspects a container using multiple approaches
171+
func PodmanInspect(containerID string) (*ContainerMetadata, error) {
172+
klog.Infof("Inspecting Podman container %s", containerID)
173+
174+
// First, try to read from filesystem directly
175+
if md, err := readContainerConfigFromFile(containerID); err == nil {
176+
klog.Infof("Successfully read container metadata from filesystem for %s", containerID)
177+
return md, nil
178+
} else {
179+
klog.V(5).Infof("Failed to read container metadata from filesystem for %s: %v", containerID, err)
180+
}
181+
182+
// If filesystem read fails, try using podman command if available
183+
if isPodmanAvailable() {
184+
klog.Infof("Trying to get container metadata using podman command for %s", containerID)
185+
// Run podman inspect command
186+
cmd := fmt.Sprintf("podman inspect %s", containerID)
187+
output, err := runCmd(cmd, podmanTimeout)
188+
if err != nil {
189+
klog.Warningf("Failed to run podman inspect for %s: %v", containerID, err)
190+
return nil, fmt.Errorf("failed to run podman inspect: %w", err)
191+
}
192+
193+
// Parse the JSON output
194+
var containers []struct {
195+
ID string `json:"Id"`
196+
Name string `json:"Name"`
197+
Image string `json:"Image"`
198+
Config struct {
199+
Labels map[string]string `json:"Labels"`
200+
Env []string `json:"Env"`
201+
} `json:"Config"`
202+
Mounts []struct {
203+
Source string `json:"Source"`
204+
Destination string `json:"Destination"`
205+
} `json:"Mounts"`
206+
NetworkSettings struct {
207+
Ports map[string][]struct {
208+
HostIP string `json:"HostIp"`
209+
HostPort string `json:"HostPort"`
210+
} `json:"Ports"`
211+
} `json:"NetworkSettings"`
212+
}
213+
214+
if err := json.Unmarshal([]byte(output), &containers); err != nil {
215+
klog.Warningf("Failed to parse podman inspect output for %s: %v", containerID, err)
216+
return nil, fmt.Errorf("failed to parse podman inspect output: %w", err)
217+
}
218+
219+
if len(containers) == 0 {
220+
klog.Warningf("No container found with ID %s", containerID)
221+
return nil, fmt.Errorf("no container found with ID %s", containerID)
222+
}
223+
224+
container := containers[0]
225+
klog.Infof("Successfully inspected container %s with name %s", container.ID, container.Name)
226+
227+
res := &ContainerMetadata{
228+
name: strings.TrimPrefix(container.Name, "/"),
229+
labels: container.Config.Labels,
230+
image: container.Image,
231+
volumes: map[string]string{},
232+
hostListens: map[string][]netaddr.IPPort{},
233+
networks: map[string]ContainerNetwork{},
234+
env: map[string]string{},
235+
}
236+
237+
// Parse volumes
238+
for _, mount := range container.Mounts {
239+
res.volumes[mount.Destination] = common.ParseKubernetesVolumeSource(mount.Source)
240+
}
241+
242+
// Parse environment variables
243+
for _, envVar := range container.Config.Env {
244+
parts := strings.SplitN(envVar, "=", 2)
245+
if len(parts) == 2 {
246+
res.env[parts[0]] = parts[1]
247+
}
248+
}
249+
250+
// Parse network settings
251+
for port, bindings := range container.NetworkSettings.Ports {
252+
if len(bindings) > 0 {
253+
ipport, err := netaddr.ParseIPPort(bindings[0].HostIP + ":" + bindings[0].HostPort)
254+
if err != nil {
255+
continue
256+
}
257+
res.hostListens[port] = append(res.hostListens[port], ipport)
258+
}
259+
}
260+
261+
// Try to find log file
262+
logPath := fmt.Sprintf("/var/lib/containers/storage/overlay-containers/%s/userdata/ctr.log", containerID)
263+
logPath = proc.HostPath(logPath)
264+
if _, err := os.Stat(logPath); err == nil {
265+
res.logPath = logPath
266+
res.logDecoder = logparser.DockerJsonDecoder{} // Podman uses same log format as Docker
267+
}
268+
269+
return res, nil
270+
}
271+
272+
// If both approaches fail, return minimal metadata
273+
klog.Warningf("Unable to get detailed metadata for Podman container %s, returning minimal metadata", containerID)
274+
return &ContainerMetadata{
275+
name: fmt.Sprintf("libpod-%s", containerID[:12]),
276+
image: "unknown",
277+
labels: map[string]string{},
278+
volumes: map[string]string{},
279+
env: map[string]string{},
280+
}, nil
281+
}

containers/registry.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ func NewRegistry(reg prometheus.Registerer, processInfoCh chan<- ProcessInfo, gp
101101
if err = CrioInit(); err != nil {
102102
klog.Warningln(err)
103103
}
104+
klog.Infoln("Initializing Podman support")
105+
if err = PodmanInit(); err != nil {
106+
klog.Warningln(err)
107+
}
104108
if err = JournaldInit(); err != nil {
105109
klog.Warningln(err)
106110
}
@@ -484,6 +488,12 @@ func calcId(cg *cgroup.Cgroup, md *ContainerMetadata) ContainerID {
484488
case cgroup.ContainerTypeTalosRuntime:
485489
return ContainerID(cg.ContainerId)
486490
case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio:
491+
case cgroup.ContainerTypeLibpod:
492+
// Handle Podman/libpod containers
493+
if cg.ContainerId == "" {
494+
return ""
495+
}
496+
return ContainerID("/libpod/" + cg.ContainerId)
487497
default:
488498
return ""
489499
}
@@ -544,7 +554,7 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) {
544554
md := &ContainerMetadata{}
545555
md.systemdTriggeredBy = SystemdTriggeredBy(cg.ContainerId)
546556
return md, nil
547-
case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio:
557+
case cgroup.ContainerTypeDocker, cgroup.ContainerTypeContainerd, cgroup.ContainerTypeSandbox, cgroup.ContainerTypeCrio, cgroup.ContainerTypeLibpod:
548558
default:
549559
return &ContainerMetadata{}, nil
550560
}
@@ -554,6 +564,9 @@ func getContainerMetadata(cg *cgroup.Cgroup) (*ContainerMetadata, error) {
554564
if cg.ContainerType == cgroup.ContainerTypeCrio {
555565
return CrioInspect(cg.ContainerId)
556566
}
567+
if cg.ContainerType == cgroup.ContainerTypeLibpod {
568+
return PodmanInspect(cg.ContainerId)
569+
}
557570
var dockerdErr error
558571
if dockerdClient != nil {
559572
md, err := DockerdInspect(cg.ContainerId)

0 commit comments

Comments
 (0)