Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cgroup/cgroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,6 +42,7 @@ const (
ContainerTypeSystemdService
ContainerTypeSandbox
ContainerTypeTalosRuntime
ContainerTypeLibpod
)

func (t ContainerType) String() string {
Expand All @@ -56,6 +59,8 @@ func (t ContainerType) String() string {
return "lxc"
case ContainerTypeSystemdService:
return "systemd"
case ContainerTypeLibpod:
return "libpod"
default:
return "unknown"
}
Expand Down Expand Up @@ -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-<ID>.scope or /machine.slice/libpod-conmon-<ID>.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
}
Expand Down
281 changes: 281 additions & 0 deletions containers/podman.go
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 14 additions & 1 deletion containers/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 ""
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down