Skip to content

Commit 0e42468

Browse files
metadata: Fix metadata association problems concerning children processes (#1232)
* Resolve all the adjacent processes IDs in the same PID namespace Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * refactor: Move funtions to semantically correct package Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * Clean up Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * [pre-commit.ci lite] apply automatic fixes * Upgrade licence text Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * [pre-commit.ci lite] apply automatic fixes * Use 2022 Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * Remove TODO Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * Remove TODOs Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * Remove unnecessary changes Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> * [pre-commit.ci lite] apply automatic fixes * Remove TODOs Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> Signed-off-by: Kemal Akkoyun <kakkoyun@gmail.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 4cf2e90 commit 0e42468

File tree

8 files changed

+311
-179
lines changed

8 files changed

+311
-179
lines changed

pkg/cgroup/cgroup.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright 2022 The Parca Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
//
14+
15+
package cgroup
16+
17+
import (
18+
"bufio"
19+
"fmt"
20+
"os"
21+
"path/filepath"
22+
"strings"
23+
"unsafe"
24+
25+
"github.com/prometheus/procfs"
26+
)
27+
28+
/*
29+
#define _GNU_SOURCE
30+
#include <stdlib.h>
31+
#include <stdio.h>
32+
#include <sys/types.h>
33+
#include <sys/stat.h>
34+
#include <fcntl.h>
35+
#include <stdint.h>
36+
37+
struct cgid_file_handle
38+
{
39+
//struct file_handle handle;
40+
unsigned int handle_bytes;
41+
int handle_type;
42+
uint64_t cgid;
43+
};
44+
45+
uint64_t get_cgroupid(char *path) {
46+
struct cgid_file_handle *h;
47+
int mount_id;
48+
int err;
49+
uint64_t ret;
50+
51+
h = malloc(sizeof(struct cgid_file_handle));
52+
if (!h)
53+
return 0;
54+
55+
h->handle_bytes = 8;
56+
err = name_to_handle_at(AT_FDCWD, path, (struct file_handle *)h, &mount_id, 0);
57+
if (err != 0) {
58+
free(h);
59+
return 0;
60+
}
61+
62+
if (h->handle_bytes != 8) {
63+
free(h);
64+
return 0;
65+
}
66+
67+
ret = h->cgid;
68+
free(h);
69+
70+
return ret;
71+
}
72+
*/
73+
import "C"
74+
75+
// FindContainerGroup returns the cgroup with the cpu controller or first systemd slice cgroup.
76+
func FindContainerGroup(cgroups []procfs.Cgroup) procfs.Cgroup {
77+
// If only 1 cgroup, simply return it
78+
if len(cgroups) == 1 {
79+
return cgroups[0]
80+
}
81+
82+
for _, cg := range cgroups {
83+
// Find first cgroup v1 with cpu controller
84+
for _, ctlr := range cg.Controllers {
85+
if ctlr == "cpu" {
86+
return cg
87+
}
88+
}
89+
90+
// Find first systemd slice
91+
// https://systemd.io/CGROUP_DELEGATION/#systemds-unit-types
92+
if strings.HasPrefix(cg.Path, "/system.slice/") || strings.HasPrefix(cg.Path, "/user.slice/") {
93+
return cg
94+
}
95+
96+
// FIXME: what are we looking for here?
97+
// https://systemd.io/CGROUP_DELEGATION/#controller-support
98+
for _, ctlr := range cg.Controllers {
99+
if strings.Contains(ctlr, "systemd") {
100+
return cg
101+
}
102+
}
103+
}
104+
105+
return procfs.Cgroup{}
106+
}
107+
108+
// PathV2AddMountpoint adds the cgroup2 mountpoint to a path.
109+
func PathV2AddMountpoint(path string) (string, error) {
110+
pathWithMountpoint := filepath.Join("/sys/fs/cgroup/unified", path)
111+
if _, err := os.Stat(pathWithMountpoint); os.IsNotExist(err) {
112+
pathWithMountpoint = filepath.Join("/sys/fs/cgroup", path)
113+
if _, err := os.Stat(pathWithMountpoint); os.IsNotExist(err) {
114+
return "", fmt.Errorf("cannot access cgroup %q: %w", path, err)
115+
}
116+
}
117+
return pathWithMountpoint, nil
118+
}
119+
120+
// ID returns the cgroup2 ID of a path.
121+
func ID(pathWithMountpoint string) (uint64, error) {
122+
cPathWithMountpoint := C.CString(pathWithMountpoint)
123+
ret := uint64(C.get_cgroupid(cPathWithMountpoint))
124+
C.free(unsafe.Pointer(cPathWithMountpoint))
125+
if ret == 0 {
126+
return 0, fmt.Errorf("GetCgroupID on %q failed", pathWithMountpoint)
127+
}
128+
return ret, nil
129+
}
130+
131+
// Paths returns the cgroup1 and cgroup2 paths of a process.
132+
// It does not include the "/sys/fs/cgroup/{unified,systemd,}" prefix.
133+
func Paths(pid int) (string, string, error) {
134+
cgroupPathV1 := ""
135+
cgroupPathV2 := ""
136+
if cgroupFile, err := os.Open(filepath.Join("/proc", fmt.Sprintf("%d", pid), "cgroup")); err == nil {
137+
defer cgroupFile.Close()
138+
139+
reader := bufio.NewReader(cgroupFile)
140+
for {
141+
line, err := reader.ReadString('\n')
142+
if err != nil {
143+
break
144+
}
145+
// Fallback in case the system the agent is running on doesn't run systemd
146+
if strings.Contains(line, ":perf_event:") {
147+
cgroupPathV1 = strings.SplitN(line, ":", 3)[2]
148+
cgroupPathV1 = strings.TrimSuffix(cgroupPathV1, "\n")
149+
continue
150+
}
151+
if strings.HasPrefix(line, "1:name=systemd:") {
152+
cgroupPathV1 = strings.TrimPrefix(line, "1:name=systemd:")
153+
cgroupPathV1 = strings.TrimSuffix(cgroupPathV1, "\n")
154+
continue
155+
}
156+
if strings.HasPrefix(line, "0::") {
157+
cgroupPathV2 = strings.TrimPrefix(line, "0::")
158+
cgroupPathV2 = strings.TrimSuffix(cgroupPathV2, "\n")
159+
continue
160+
}
161+
}
162+
} else {
163+
return "", "", fmt.Errorf("cannot parse cgroup: %w", err)
164+
}
165+
166+
if cgroupPathV1 == "/" {
167+
cgroupPathV1 = ""
168+
}
169+
170+
if cgroupPathV2 == "/" {
171+
cgroupPathV2 = ""
172+
}
173+
174+
if cgroupPathV2 == "" && cgroupPathV1 == "" {
175+
return "", "", fmt.Errorf("cannot find cgroup path in /proc/PID/cgroup")
176+
}
177+
178+
return cgroupPathV1, cgroupPathV2, nil
179+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// limitations under the License.
1313
//
1414

15-
package metadata
15+
package cgroup
1616

1717
import (
1818
"testing"
@@ -190,7 +190,7 @@ func TestFindFirstCPUCgroup(t *testing.T) {
190190

191191
for _, tt := range tests {
192192
t.Run(tt.name, func(t *testing.T) {
193-
got := findFirstCPUCgroup(tt.cgroups)
193+
got := FindContainerGroup(tt.cgroups)
194194
if tt.wantIndex < 0 {
195195
require.Equal(t, procfs.Cgroup{}, got)
196196
} else {

pkg/discovery/kubernetes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import (
1818
"fmt"
1919

2020
"github.com/go-kit/log"
21+
"github.com/go-kit/log/level"
2122
"github.com/prometheus/common/model"
2223
"github.com/prometheus/prometheus/util/strutil"
2324
v1 "k8s.io/api/core/v1"
2425

2526
"github.com/parca-dev/parca-agent/pkg/discovery/kubernetes"
27+
"github.com/parca-dev/parca-agent/pkg/namespace"
2628
)
2729

2830
type PodConfig struct {
@@ -124,7 +126,13 @@ func (g *PodDiscoverer) buildGroup(pod *v1.Pod, containers []*kubernetes.Contain
124126
"container": model.LabelValue(container.ContainerName),
125127
"containerid": model.LabelValue(container.ContainerID),
126128
})
129+
adjacentPIDs, err := namespace.PIDNamespaceAdjacentPIDs(container.PID) // linux namespace
130+
if err != nil {
131+
g.logger.Log("msg", "failed to get adjacent pids", "err", err)
132+
}
127133
tg.PIDs = append(tg.PIDs, container.PID)
134+
tg.PIDs = append(tg.PIDs, adjacentPIDs...)
135+
level.Debug(g.logger).Log("msg", "found pids", "pids", fmt.Sprintf("%v", tg.PIDs))
128136
}
129137

130138
return tg

pkg/discovery/kubernetes/containerruntimes/containerruntimes.go

Lines changed: 0 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -14,156 +14,19 @@
1414
package containerruntimes
1515

1616
import (
17-
"bufio"
1817
"encoding/json"
1918
"fmt"
20-
"os"
21-
"path/filepath"
2219
"regexp"
23-
"strings"
24-
"syscall"
25-
"unsafe"
2620

2721
ocispec "github.com/opencontainers/runtime-spec/specs-go"
2822
)
2923

30-
/*
31-
#define _GNU_SOURCE
32-
#include <stdlib.h>
33-
#include <stdio.h>
34-
#include <sys/types.h>
35-
#include <sys/stat.h>
36-
#include <fcntl.h>
37-
#include <stdint.h>
38-
39-
struct cgid_file_handle
40-
{
41-
//struct file_handle handle;
42-
unsigned int handle_bytes;
43-
int handle_type;
44-
uint64_t cgid;
45-
};
46-
47-
uint64_t get_cgroupid(char *path) {
48-
struct cgid_file_handle *h;
49-
int mount_id;
50-
int err;
51-
uint64_t ret;
52-
53-
h = malloc(sizeof(struct cgid_file_handle));
54-
if (!h)
55-
return 0;
56-
57-
h->handle_bytes = 8;
58-
err = name_to_handle_at(AT_FDCWD, path, (struct file_handle *)h, &mount_id, 0);
59-
if (err != 0) {
60-
free(h);
61-
return 0;
62-
}
63-
64-
if (h->handle_bytes != 8) {
65-
free(h);
66-
return 0;
67-
}
68-
69-
ret = h->cgid;
70-
free(h);
71-
72-
return ret;
73-
}
74-
*/
75-
import "C"
76-
7724
// CRIClient defines the interface to interact with the container runtime interfaces.
7825
type CRIClient interface {
7926
Close() error
8027
PIDFromContainerID(containerID string) (int, error)
8128
}
8229

83-
func CgroupPathV2AddMountpoint(path string) (string, error) {
84-
pathWithMountpoint := filepath.Join("/sys/fs/cgroup/unified", path)
85-
if _, err := os.Stat(pathWithMountpoint); os.IsNotExist(err) {
86-
pathWithMountpoint = filepath.Join("/sys/fs/cgroup", path)
87-
if _, err := os.Stat(pathWithMountpoint); os.IsNotExist(err) {
88-
return "", fmt.Errorf("cannot access cgroup %q: %w", path, err)
89-
}
90-
}
91-
return pathWithMountpoint, nil
92-
}
93-
94-
// GetCgroupID returns the cgroup2 ID of a path.
95-
func GetCgroupID(pathWithMountpoint string) (uint64, error) {
96-
cPathWithMountpoint := C.CString(pathWithMountpoint)
97-
ret := uint64(C.get_cgroupid(cPathWithMountpoint))
98-
C.free(unsafe.Pointer(cPathWithMountpoint))
99-
if ret == 0 {
100-
return 0, fmt.Errorf("GetCgroupID on %q failed", pathWithMountpoint)
101-
}
102-
return ret, nil
103-
}
104-
105-
// GetCgroupPaths returns the cgroup1 and cgroup2 paths of a process.
106-
// It does not include the "/sys/fs/cgroup/{unified,systemd,}" prefix.
107-
func GetCgroupPaths(pid int) (string, string, error) {
108-
cgroupPathV1 := ""
109-
cgroupPathV2 := ""
110-
if cgroupFile, err := os.Open(filepath.Join("/proc", fmt.Sprintf("%d", pid), "cgroup")); err == nil {
111-
defer cgroupFile.Close()
112-
113-
reader := bufio.NewReader(cgroupFile)
114-
for {
115-
line, err := reader.ReadString('\n')
116-
if err != nil {
117-
break
118-
}
119-
// Fallback in case the system the agent is running on doesn't run systemd
120-
if strings.Contains(line, ":perf_event:") {
121-
cgroupPathV1 = strings.SplitN(line, ":", 3)[2]
122-
cgroupPathV1 = strings.TrimSuffix(cgroupPathV1, "\n")
123-
continue
124-
}
125-
if strings.HasPrefix(line, "1:name=systemd:") {
126-
cgroupPathV1 = strings.TrimPrefix(line, "1:name=systemd:")
127-
cgroupPathV1 = strings.TrimSuffix(cgroupPathV1, "\n")
128-
continue
129-
}
130-
if strings.HasPrefix(line, "0::") {
131-
cgroupPathV2 = strings.TrimPrefix(line, "0::")
132-
cgroupPathV2 = strings.TrimSuffix(cgroupPathV2, "\n")
133-
continue
134-
}
135-
}
136-
} else {
137-
return "", "", fmt.Errorf("cannot parse cgroup: %w", err)
138-
}
139-
140-
if cgroupPathV1 == "/" {
141-
cgroupPathV1 = ""
142-
}
143-
144-
if cgroupPathV2 == "/" {
145-
cgroupPathV2 = ""
146-
}
147-
148-
if cgroupPathV2 == "" && cgroupPathV1 == "" {
149-
return "", "", fmt.Errorf("cannot find cgroup path in /proc/PID/cgroup")
150-
}
151-
152-
return cgroupPathV1, cgroupPathV2, nil
153-
}
154-
155-
func GetMntNs(pid int) (uint64, error) {
156-
fileinfo, err := os.Stat(filepath.Join("/proc", fmt.Sprintf("%d", pid), "ns/mnt"))
157-
if err != nil {
158-
return 0, err
159-
}
160-
stat, ok := fileinfo.Sys().(*syscall.Stat_t)
161-
if !ok {
162-
return 0, fmt.Errorf("not a syscall.Stat_t")
163-
}
164-
return stat.Ino, nil
165-
}
166-
16730
func ParseOCIState(stateBuf []byte) (string, int, error) {
16831
ociState := &ocispec.State{}
16932
if err := json.Unmarshal(stateBuf, ociState); err != nil {

0 commit comments

Comments
 (0)