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
29 changes: 28 additions & 1 deletion 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|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
Expand All @@ -40,6 +42,7 @@ const (
ContainerTypeSystemdService
ContainerTypeSandbox
ContainerTypeTalosRuntime
ContainerTypePodman
)

func (t ContainerType) String() string {
Expand All @@ -56,6 +59,8 @@ func (t ContainerType) String() string {
return "lxc"
case ContainerTypeSystemdService:
return "systemd"
case ContainerTypePodman:
return "podman"
default:
return "unknown"
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
75 changes: 75 additions & 0 deletions cgroup/cgroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions cgroup/fixtures/proc/4000/cgroup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0::/machine.slice/libpod-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.scope
1 change: 1 addition & 0 deletions cgroup/fixtures/proc/4100/cgroup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0::/user.slice/user-1000.slice/user@1000.service/libpod-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.scope
1 change: 1 addition & 0 deletions cgroup/fixtures/proc/4200/cgroup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0::/system.slice/myapp.service/libpod-payload-cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
1 change: 1 addition & 0 deletions cgroup/fixtures/proc/4300/cgroup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0::/machine.slice/libpod-conmon-dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd.scope
52 changes: 42 additions & 10 deletions containers/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}
}
}

Expand Down
160 changes: 160 additions & 0 deletions containers/podman.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading