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
2 changes: 1 addition & 1 deletion admin/commands/management/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type RegisterCommand struct {
flags.MetricsModeFlags

Address string `name:"node-address" arg:"" default:"${nodeIp}" help:"Node address (autodetected, default: ${nodeIp})"`
NodeType string `arg:"" enum:"generic,container" default:"generic" help:"Node type. One of: [${enum}]. Default: ${default}"`
NodeType string `arg:"" enum:"generic,container" default:"${nodeTypeDefault}" help:"Node type. One of: [${enum}]. Default: ${default}"`
NodeName string `arg:"" default:"${hostname}" help:"Node name (autodetected, default: ${hostname})"`
MachineID string `default:"${defaultMachineID}" help:"Node machine-id (autodetected, default: ${defaultMachineID})"`
Distro string `default:"${distro}" help:"Node OS distribution (autodetected, default: ${distro})"`
Expand Down
4 changes: 2 additions & 2 deletions managed/models/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,7 @@ func setupPMMServerHAAgents(q *reform.Querier, params SetupDBParams) error {
"environment": "pmm",
}

node, err := createNodeWithID(q, nodeID, GenericNodeType, &CreateNodeParams{
node, err := createNodeWithID(q, nodeID, ContainerNodeType, &CreateNodeParams{
NodeName: params.HANodeID,
Address: LocalhostAddr,
CustomLabels: labels,
Expand Down Expand Up @@ -1669,7 +1669,7 @@ func setupPMMServerHAAgents(q *reform.Querier, params SetupDBParams) error {

func setupPMMServerAgents(q *reform.Querier, params SetupDBParams) error {
// create PMM Server Node and associated Agents
node, err := createNodeWithID(q, PMMServerNodeID, GenericNodeType, &CreateNodeParams{
node, err := createNodeWithID(q, PMMServerNodeID, ContainerNodeType, &CreateNodeParams{
NodeName: "pmm-server",
Address: LocalhostAddr,
IsPMMServerNode: true,
Expand Down
10 changes: 9 additions & 1 deletion managed/models/node_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func TestNodeHelpers(t *testing.T) {
UpdatedAt: now,
}, {
NodeID: models.PMMServerNodeID,
NodeType: models.GenericNodeType,
NodeType: models.ContainerNodeType,
NodeName: "pmm-server",
Address: "127.0.0.1",
CreatedAt: now,
Expand All @@ -216,6 +216,14 @@ func TestNodeHelpers(t *testing.T) {
MachineID: new("MySQLNode"),
CreatedAt: now,
UpdatedAt: now,
}, {
NodeID: models.PMMServerNodeID,
NodeType: models.ContainerNodeType,
NodeName: "pmm-server",
Address: "127.0.0.1",
CreatedAt: now,
UpdatedAt: now,
IsPMMServerNode: true,
},
}
require.Equal(t, expected, nodes)
Expand Down
8 changes: 4 additions & 4 deletions managed/services/management/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ func TestNodeService(t *testing.T) {
Nodes: []*managementv1.UniversalNode{
{
NodeId: "pmm-server",
NodeType: "generic",
NodeType: "container",
NodeName: "pmm-server",
MachineId: "",
Distro: "",
Expand Down Expand Up @@ -401,15 +401,15 @@ func TestNodeService(t *testing.T) {
s.r.(*mockAgentsRegistry).On("IsConnected", nodeExporterID).Return(true).Once()

res, err := s.ListNodes(ctx, &managementv1.ListNodesRequest{
NodeType: inventoryv1.NodeType_NODE_TYPE_GENERIC_NODE,
NodeType: inventoryv1.NodeType_NODE_TYPE_CONTAINER_NODE,
})
require.NoError(t, err)

expected := &managementv1.ListNodesResponse{
Nodes: []*managementv1.UniversalNode{
{
NodeId: "pmm-server",
NodeType: "generic",
NodeType: "container",
NodeName: "pmm-server",
MachineId: "",
Distro: "",
Expand Down Expand Up @@ -590,7 +590,7 @@ func TestNodeService(t *testing.T) {
expected := &managementv1.GetNodeResponse{
Node: &managementv1.UniversalNode{
NodeId: "pmm-server",
NodeType: "generic",
NodeType: "container",
NodeName: "pmm-server",
MachineId: "",
Distro: "",
Expand Down
2 changes: 1 addition & 1 deletion managed/services/qan/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ func TestClientPerformance(t *testing.T) {
ServiceName: "test-mysql",
NodeId: "pmm-server",
NodeName: "pmm-server",
NodeType: "generic",
NodeType: "container",
ServiceId: "0d350868-4d85-4884-b972-dff130129c23",
ServiceType: "mysql",
AgentId: "6b74c6bf-642d-43f0-bee1-0faddd1a2e28",
Expand Down
43 changes: 38 additions & 5 deletions utils/nodeinfo/nodeinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,25 @@ package nodeinfo
import (
"net"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
)

// containerMarkerFiles are files created inside the container: Docker creates /.dockerenv, Podman
// creates /run/.containerenv, and systemd running as PID 1 in a container (LXC, LXD,
// systemd-nspawn) writes the runtime name to /run/systemd/container. The last one matters because
// systemd does not pass its own "container" variable on to the services it starts, so an agent
// running as a unit cannot see it.
var containerMarkerFiles = []string{".dockerenv", "run/.containerenv", "run/systemd/container"}

// containerCgroupMarkers are substrings of the /proc/1/cgroup paths under cgroup v1, where those
// paths carry the runtime name and the container ID; "docker-" catches the systemd cgroup driver,
// which nests containers as /system.slice/docker-<id>.scope. Under cgroup v2 the file usually
// holds just "0::/", so it can confirm a container but never rule one out.
var containerCgroupMarkers = []string{"/docker/", "docker-", "/lxc/", "/kubepods", "containerd", "crio-", "libpod"}

// NodeInfo contains node information.
type NodeInfo struct {
Container bool
Expand All @@ -35,17 +50,35 @@ type NodeInfo struct {
// Get returns node information for current node.
func Get() *NodeInfo {
return &NodeInfo{
Container: checkContainer(),
Container: checkContainer("/"),
Distro: readDistro(),
MachineID: readMachineID(),
PublicAddress: readPublicAddress(),
}
}

func checkContainer() bool {
// https://stackoverflow.com/a/20012536
b, _ := os.ReadFile("/proc/1/cgroup")
return strings.Contains(string(b), "/docker/") || strings.Contains(string(b), "/lxc/")
// checkContainer reports whether the current process runs inside a container.
// The root argument is the filesystem root to probe; it is "/" outside of tests.
func checkContainer(root string) bool {
for _, name := range containerMarkerFiles {
_, err := os.Stat(filepath.Join(root, name))
if err == nil {
return true
}
}

// Podman and LXC set "container" for the processes they start; Kubernetes injects its service
// host into every Pod.
if os.Getenv("container") != "" || os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
return true
}

b, _ := os.ReadFile(filepath.Join(root, "proc/1/cgroup")) //nolint:gosec
cgroup := string(b)

return slices.ContainsFunc(containerCgroupMarkers, func(marker string) bool {
return strings.Contains(cgroup, marker)
})
}

func readDistro() string {
Expand Down
75 changes: 74 additions & 1 deletion utils/nodeinfo/nodeinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package nodeinfo

import (
"net"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
Expand All @@ -28,7 +30,6 @@ func TestGet(t *testing.T) {
t.Parallel()

info := Get()
require.False(t, info.Container, "not expected to be run inside a container")
assert.Equal(t, runtime.GOOS, info.Distro)

// all our test environments have IPv4 addresses
Expand All @@ -38,3 +39,75 @@ func TestGet(t *testing.T) {

assert.False(t, strings.HasSuffix(info.MachineID, "\n"), "%q", info.MachineID)
}

func TestCheckContainer(t *testing.T) {
for _, tc := range []struct {
name string
files map[string]string
env map[string]string
expected bool
}{
{
name: "host with cgroup v2",
files: map[string]string{"proc/1/cgroup": "0::/init.scope\n"},
}, {
name: "host with cgroup v1",
files: map[string]string{"proc/1/cgroup": "1:name=systemd:/init.scope\n0::/init.scope\n"},
}, {
// the case PMM Server itself hits: cgroup v2 says nothing, only the marker file does
name: "docker with cgroup v2",
files: map[string]string{".dockerenv": "", "proc/1/cgroup": "0::/\n"},
expected: true,
}, {
name: "docker with cgroup v1",
files: map[string]string{"proc/1/cgroup": "1:name=systemd:/docker/dc4b1a5cb7fd\n"},
expected: true,
}, {
name: "podman",
files: map[string]string{"run/.containerenv": "engine=\"podman-5.4.0\"\n", "proc/1/cgroup": "0::/\n"},
expected: true,
}, {
name: "docker with the systemd cgroup driver",
files: map[string]string{"proc/1/cgroup": "1:name=systemd:/system.slice/docker-dc4b1a5cb7fd.scope\n"},
expected: true,
}, {
name: "lxc",
files: map[string]string{"proc/1/cgroup": "0::/\n"},
env: map[string]string{"container": "lxc"},
expected: true,
}, {
// systemd strips its own "container" variable from the services it starts, so an agent
// running as a unit only has the file to go by
name: "lxc with an agent started by systemd",
files: map[string]string{"run/systemd/container": "lxc\n", "proc/1/cgroup": "0::/\n"},
expected: true,
}, {
name: "kubernetes pod with cgroup v2",
files: map[string]string{"proc/1/cgroup": "0::/\n"},
env: map[string]string{"KUBERNETES_SERVICE_HOST": "10.96.0.1"},
expected: true,
}, {
name: "kubernetes pod with cgroup v1",
files: map[string]string{"proc/1/cgroup": "1:name=systemd:/kubepods/besteffort/pod9f4a\n"},
expected: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
// keep the outcome independent of the environment the tests themselves run in
t.Setenv("container", "")
t.Setenv("KUBERNETES_SERVICE_HOST", "")
for name, value := range tc.env {
t.Setenv(name, value)
}

root := t.TempDir()
for name, content := range tc.files {
path := filepath.Join(root, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}

assert.Equal(t, tc.expected, checkContainer(root))
})
}
}
Loading