Skip to content

Commit

Permalink
fix: Detect Docker container id on cgroup v2. (#1943)
Browse files Browse the repository at this point in the history
  • Loading branch information
tippmar-nr committed Oct 2, 2023
1 parent a4499a1 commit 9c7e114
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 48 deletions.
139 changes: 91 additions & 48 deletions src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs
Expand Up @@ -24,7 +24,8 @@ public class VendorInfo
{
private const string ValidateMetadataRegex = @"^[a-zA-Z0-9-_. /]*$";
#if NETSTANDARD2_0
private const string ContainerIdRegex = @"[0-9a-f]{64}";
private const string ContainerIdV1Regex = @".*:cpu:/docker/([0-9a-f]{64}).*";
private const string ContainerIdV2Regex = ".*/docker/containers/([0-9a-f]{64})/.*";
#endif

private const string AwsName = @"aws";
Expand Down Expand Up @@ -95,11 +96,16 @@ public VendorInfo(IConfiguration configuration, IAgentHealthReporter agentHealth
// If Docker info is set to be checked, it must be checked for all vendors.
if (_configuration.UtilizationDetectDocker)
{
var dockerVendorInfo = GetDockerVendorInfo();
if (dockerVendorInfo != null)
#if NETSTANDARD2_0
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper());
if (dockerVendorInfo != null)
{
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
}
}
#endif
}

if (_configuration.UtilizationDetectKubernetes)
Expand Down Expand Up @@ -270,53 +276,75 @@ private string GetProcessEnvironmentVariable(string variableName)
}
}

private IVendorModel GetDockerVendorInfo()
{
#if NETSTANDARD2_0
bool isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
int subsystemsIndex = 1;
int controlGroupIndex = 2;

if (isLinux)
{
try
{
string id = null;
var fileLines = File.ReadAllLines("/proc/self/cgroup");

foreach(var line in fileLines)
{
var elements = line.Split(StringSeparators.Colon);
var cpuSubsystem = elements[subsystemsIndex].Split(StringSeparators.Comma).FirstOrDefault(subsystem => subsystem == "cpu");
if (cpuSubsystem != null)
{
var controlGroup = elements[controlGroupIndex];
var match = Regex.Match(controlGroup, ContainerIdRegex);

if (match.Success)
{
id = match.Value;
}
}
}

if(id == null)
{
return null;
}

return new DockerVendorModel(id);
}
catch
{
return null;
}

}
#endif
return null;
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
{
IVendorModel vendorModel = null;
try
{
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/mountinfo");
vendorModel = TryGetDockerCGroupV2(fileContent);
if (vendorModel == null)
Log.Finest("Found /proc/self/mountinfo but failed to parse Docker container id.");

}
catch (Exception ex)
{
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/mountinfo.");
}

if (vendorModel == null) // fall back to the v1 check if v2 wasn't successful
{
try
{
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/cgroup");
vendorModel = TryGetDockerCGroupV1(fileContent);
if (vendorModel == null)
Log.Finest("Found /proc/self/cgroup but failed to parse Docker container id.");
}
catch (Exception ex)
{
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/cgroup.");
return null;
}
}

return vendorModel;
}

private IVendorModel TryGetDockerCGroupV1(string fileContent)
{
string id = null;
var matches = Regex.Matches(fileContent, ContainerIdV1Regex);
if (matches.Count > 0)
{
var firstMatch = matches[0];
if (firstMatch.Success && firstMatch.Groups.Count > 1 && firstMatch.Groups[1].Success)
{
id = firstMatch.Groups[1].Value;
}
}

return id == null ? null : new DockerVendorModel(id);
}

private IVendorModel TryGetDockerCGroupV2(string fileContent)
{
string id = null;
var matches = Regex.Matches(fileContent, ContainerIdV2Regex);
if (matches.Count > 0)
{
var firstMatch = matches[0];
if (firstMatch.Success && firstMatch.Groups.Count > 1 && firstMatch.Groups[1].Success)
{
id = firstMatch.Groups[1].Value;
}
}

return id == null ? null : new DockerVendorModel(id);
}
#endif

public IVendorModel GetKubernetesInfo()
{
var envVar = _environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
Expand Down Expand Up @@ -376,4 +404,19 @@ public bool IsValidMetadata(string data)
return Regex.IsMatch(data, ValidateMetadataRegex);
}
}
#if NETSTANDARD2_0
// needed for unit testing only
public interface IFileReaderWrapper
{
string ReadAllText(string fileName);
}

public class FileReaderWrapper : IFileReaderWrapper
{
public string ReadAllText(string fileName)
{
return File.ReadAllText(fileName);
}
}
#endif
}
122 changes: 122 additions & 0 deletions tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs
Expand Up @@ -2,9 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

using System;
using System.IO;
using System.Linq;
using NewRelic.Agent.Core.AgentHealth;
using NewRelic.Agent.Configuration;
using NewRelic.Agent.TestUtilities;
using NewRelic.SystemInterfaces;
using NUnit.Framework;
using Telerik.JustMock;
Expand Down Expand Up @@ -280,6 +282,126 @@ public void GetVendors_GetKubernetesVendorInfo_None()
Assert.Null(model);
}

#if NET
[Test]
public void GetVendors_GetDockerVendorInfo_ParsesV2()
{
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns(@"
1425 1301 0:290 / / rw,relatime master:314 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/SEESBOIUB4X3HZXQDX5TSEQ7BN:/var/lib/docker/overlay2/l/MOPJN3KMFAIZGI5ENZ4O34OONV:/var/lib/docker/overlay2/l/HDHJSGZM5PTRBYHW5EAYHS7XRU:/var/lib/docker/overlay2/l/DPNQ4BZTYI2XJTICBFBZQ3LYGY:/var/lib/docker/overlay2/l/WHFN2B5YEUTYPT77F26T57WB5I:/var/lib/docker/overlay2/l/P7VISFMMKEWRYA7L34PW2O2J54:/var/lib/docker/overlay2/l/ZWNBERDCDMC6LTZHJ4L64AC5LD:/var/lib/docker/overlay2/l/UGWQJ4NGWITVZZNEXAK7ZHDQDD:/var/lib/docker/overlay2/l/IZ5XCLZYFBF7BC4XULL7IJWT3Q:/var/lib/docker/overlay2/l/EGK3Y3BMJAVWDQZLM4DFYAZQNJ:/var/lib/docker/overlay2/l/LNHVYS3UDT2S2TTN2TF3JVSHFH,upperdir=/var/lib/docker/overlay2/14399ff93af039f15ee6a9633110eaf5ac552802c589e7c5595e32adfb635d39/diff,workdir=/var/lib/docker/overlay2/14399ff93af039f15ee6a9633110eaf5ac552802c589e7c5595e32adfb635d39/work
1426 1425 0:293 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
1427 1425 0:294 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
1428 1427 0:295 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
1429 1425 0:296 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
1453 1429 0:297 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,mode=755
1454 1453 0:56 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:75 - cgroup cpuset rw,cpuset
1455 1453 0:57 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/cpu ro,nosuid,nodev,noexec,relatime master:76 - cgroup cpu rw,cpu
1456 1453 0:58 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/cpuacct ro,nosuid,nodev,noexec,relatime master:77 - cgroup cpuacct rw,cpuacct
1457 1453 0:59 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/blkio ro,nosuid,nodev,noexec,relatime master:78 - cgroup blkio rw,blkio
1458 1453 0:60 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:79 - cgroup memory rw,memory
1459 1453 0:61 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/devices ro,nosuid,nodev,noexec,relatime master:80 - cgroup devices rw,devices
1460 1453 0:62 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/freezer ro,nosuid,nodev,noexec,relatime master:81 - cgroup freezer rw,freezer
1461 1453 0:63 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/net_cls ro,nosuid,nodev,noexec,relatime master:82 - cgroup net_cls rw,net_cls
1462 1453 0:64 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/perf_event ro,nosuid,nodev,noexec,relatime master:83 - cgroup perf_event rw,perf_event
1463 1453 0:65 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/net_prio ro,nosuid,nodev,noexec,relatime master:84 - cgroup net_prio rw,net_prio
1464 1453 0:66 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/hugetlb ro,nosuid,nodev,noexec,relatime master:85 - cgroup hugetlb rw,hugetlb
1465 1453 0:67 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/pids ro,nosuid,nodev,noexec,relatime master:86 - cgroup pids rw,pids
1466 1453 0:68 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/rdma ro,nosuid,nodev,noexec,relatime master:87 - cgroup rdma rw,rdma
1467 1453 0:69 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/misc ro,nosuid,nodev,noexec,relatime master:88 - cgroup misc rw,misc
1468 1453 0:132 /docker/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb /sys/fs/cgroup/systemd ro,nosuid,nodev,noexec,relatime master:89 - cgroup cgroup rw,name=systemd
1469 1427 0:292 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
1470 1427 0:298 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k
1471 1425 8:64 /data/docker/containers/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/sde rw,discard,errors=remount-ro,data=ordered
1472 1425 8:64 /data/docker/containers/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb/hostname /etc/hostname rw,relatime - ext4 /dev/sde rw,discard,errors=remount-ro,data=ordered
1473 1425 8:64 /data/docker/containers/adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb/hosts /etc/hosts rw,relatime - ext4 /dev/sde rw,discard,errors=remount-ro,data=ordered
1312 1426 0:293 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
1313 1426 0:293 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
1314 1426 0:293 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
1315 1426 0:293 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
1316 1426 0:299 / /proc/acpi ro,relatime - tmpfs tmpfs ro
1339 1426 0:294 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
1340 1426 0:294 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
1341 1426 0:294 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
1342 1429 0:300 / /sys/firmware ro,relatime - tmpfs tmpfs ro
");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
Assert.NotNull(model);
Assert.AreEqual("adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb", model.Id);
}

[Test]
public void GetVendors_GetDockerVendorInfo_ParsesV1_IfV2LookupFailsToParseFile()
{
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("foo bar baz");
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns(@"
15:name=systemd:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
14:misc:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
13:rdma:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
12:pids:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
11:hugetlb:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
10:net_prio:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
9:perf_event:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
8:net_cls:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
7:freezer:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
6:devices:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
5:memory:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
4:blkio:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
3:cpuacct:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
2:cpu:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
Assert.NotNull(model);
Assert.AreEqual("b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043", model.Id);
}


[Test]
public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist()
{
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Throws<FileNotFoundException>();
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns(@"
15:name=systemd:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
14:misc:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
13:rdma:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
12:pids:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
11:hugetlb:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
10:net_prio:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
9:perf_event:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
8:net_cls:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
7:freezer:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
6:devices:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
5:memory:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
4:blkio:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
3:cpuacct:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
2:cpu:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
Assert.NotNull(model);
Assert.AreEqual("b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043", model.Id);
}

[Test]
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2()
{
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("blah blah blah");
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns("foo bar baz");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
Assert.Null(model);
}
#endif
private void SetEnvironmentVariable(string variableName, string value, EnvironmentVariableTarget environmentVariableTarget)
{
Mock.Arrange(() => _environment.GetEnvironmentVariable(variableName, environmentVariableTarget)).Returns(value);
Expand Down

0 comments on commit 9c7e114

Please sign in to comment.