Skip to content

Commit

Permalink
feat: Add support for capturing container id from AWS ECS. (#2481)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaffinito committed Jun 12, 2024
1 parent 27d0ed0 commit c018b8a
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 31 deletions.
89 changes: 67 additions & 22 deletions src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

#if NETSTANDARD2_0
using System.IO;
using System.Runtime.InteropServices;
#endif

namespace NewRelic.Agent.Core.Utilization
{
public class VendorInfo
{
private const string ValidateMetadataRegex = @"^[a-zA-Z0-9-_. /]*$";
#if NETSTANDARD2_0
private const string ContainerIdV1Regex = @".*cpu.*([0-9a-f]{64})";
private const string ContainerIdV2Regex = ".*/docker/containers/([0-9a-f]{64})/.*";
#endif
private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI";
private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4";

private const string AwsName = @"aws";
private const string AzureName = @"azure";
private const string GcpName = @"gcp";
private const string PcfName = @"pcf";
private const string DockerName = @"docker";
private const string KubernetesName = @"kubernetes";
private const string EcsFargateName = @"ecs-fargate";

private readonly string AwsTokenUri = @"http://169.254.169.254/latest/api/token";
private readonly string AwsMetadataUri = @"http://169.254.169.254/latest/dynamic/instance-identity/document";
Expand Down Expand Up @@ -96,16 +93,11 @@ public IDictionary<string, IVendorModel> GetVendors()
// If Docker info is set to be checked, it must be checked for all vendors.
if (_configuration.UtilizationDetectDocker)
{
#if NETSTANDARD2_0
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper(), IsLinux());
if (dockerVendorInfo != null)
{
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper());
if (dockerVendorInfo != null)
{
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
}
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
}
#endif
}

if (_configuration.UtilizationDetectKubernetes)
Expand Down Expand Up @@ -276,10 +268,11 @@ private string GetProcessEnvironmentVariable(string variableName)
}
}

#if NETSTANDARD2_0
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper, bool isLinux)
{
IVendorModel vendorModel = null;
IVendorModel vendorModel = null;
if (isLinux)
{
try
{
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/mountinfo");
Expand All @@ -305,11 +298,47 @@ public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
catch (Exception ex)
{
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/cgroup.");
return null;
}
}
}

return vendorModel;
if (vendorModel == null)
{
try
{
var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV4EnvVar);
if (!string.IsNullOrWhiteSpace(metadataUri))
{
vendorModel = TryGetEcsFargateDockerId(metadataUri);
if (vendorModel == null)
Log.Finest($"Found {AwsEcsMetadataV4EnvVar} but failed to parse Docker container id.");
}
}
catch (Exception ex)
{
Log.Finest(ex, $"Failed to parse Docker container id from {AwsEcsMetadataV4EnvVar}.");
}
}

if (vendorModel == null)
{
try
{
var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV3EnvVar);
if (!string.IsNullOrWhiteSpace(metadataUri))
{
vendorModel = TryGetEcsFargateDockerId(metadataUri);
if (vendorModel == null)
Log.Finest($"Found {AwsEcsMetadataV3EnvVar} but failed to parse Docker container id.");
}
}
catch (Exception ex)
{
Log.Finest(ex, $"Failed to parse Docker container id from {AwsEcsMetadataV3EnvVar}.");
}
}

return vendorModel;
}

private IVendorModel TryGetDockerCGroupV1(string fileContent)
Expand Down Expand Up @@ -353,7 +382,15 @@ private IVendorModel TryGetDockerCGroupV2(string fileContent)

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

private IVendorModel TryGetEcsFargateDockerId(string metadataUri)
{
var responseJson = _vendorHttpApiRequestor.CallVendorApi(new Uri(metadataUri), GetMethod, EcsFargateName);
var jObject = JObject.Parse(responseJson);
var idToken = jObject.SelectToken("DockerId");
var id = NormalizeAndValidateMetadata((string)idToken, "DockerId", EcsFargateName);
return id == null ? null : new DockerVendorModel(id);
}

public IVendorModel GetKubernetesInfo()
{
Expand Down Expand Up @@ -413,8 +450,17 @@ public bool IsValidMetadata(string data)
{
return Regex.IsMatch(data, ValidateMetadataRegex);
}
}

private static bool IsLinux()
{
#if NETSTANDARD2_0
return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
#else
return false; // No Linux on .NET Framework
#endif
}
}

// needed for unit testing only
public interface IFileReaderWrapper
{
Expand All @@ -428,5 +474,4 @@ public string ReadAllText(string fileName)
return File.ReadAllText(fileName);
}
}
#endif
}
143 changes: 134 additions & 9 deletions tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using NewRelic.Agent.Core.AgentHealth;
using NewRelic.Agent.Configuration;
using NewRelic.Agent.TestUtilities;
using NewRelic.Agent.Core.AgentHealth;
using NewRelic.SystemInterfaces;
using NUnit.Framework;
using Telerik.JustMock;
Expand All @@ -27,6 +27,8 @@ public class VendorInfoTests
private const string PcfInstanceIp = @"CF_INSTANCE_IP";
private const string PcfMemoryLimit = @"MEMORY_LIMIT";
private const string KubernetesServiceHost = @"KUBERNETES_SERVICE_HOST";
private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI";
private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4";

[SetUp]
public void Setup()
Expand Down Expand Up @@ -338,7 +340,7 @@ public void GetVendors_GetDockerVendorInfo_ParsesV2()
1342 1429 0:300 / /sys/firmware ro,relatime - tmpfs tmpfs ro
");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo("adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb"));
}
Expand Down Expand Up @@ -367,7 +369,7 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfV2LookupFailsToParseFile()
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo("b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043"));
}
Expand Down Expand Up @@ -432,7 +434,7 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_ForCustomerIssue()
1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod04f9c4b4_5e71_4a0a_aa3a_f62f089e3f73.slice/cri-containerd-b10c13eeeea82c495c9e2fbb07ab448024715fdd55218e22cce6cd815c84bd58.scope
");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo("b10c13eeeea82c495c9e2fbb07ab448024715fdd55218e22cce6cd815c84bd58"));
}
Expand Down Expand Up @@ -462,20 +464,143 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist()
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");

var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo("b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043"));
}

[Test]
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2()
[TestCase(true)]
[TestCase(false)]
public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV4_IfUnableToParseV1OrV2(bool isLinux)
{
// This docker ID is in the Fargate format, but the test is still valid for non-Fargate ECS hosts.
var dockerId = "1e1698469422439ea356071e581e8545-2769485393";
SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, $"http://169.254.170.2/v4/{dockerId}", EnvironmentVariableTarget.Process);
Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.IsAny<Uri>(), Arg.AnyString, Arg.AnyString, Arg.IsNull<IEnumerable<string>>())).Returns("""
{
"DockerId": "1e1698469422439ea356071e581e8545-2769485393",
"Name": "fargateapp",
"DockerName": "fargateapp",
"Image": "123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest",
"ImageID": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd",
"Labels": {
"com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:123456789012:cluster/testcluster",
"com.amazonaws.ecs.container-name": "fargateapp",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545",
"com.amazonaws.ecs.task-definition-family": "fargatetestapp",
"com.amazonaws.ecs.task-definition-version": "7"
},
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Limits": {
"CPU": 2
},
"CreatedAt": "2024-04-25T17:38:31.073208914Z",
"StartedAt": "2024-04-25T17:38:31.073208914Z",
"Type": "NORMAL",
"LogDriver": "awslogs",
"LogOptions": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/fargatetestapp",
"awslogs-region": "us-west-2",
"awslogs-stream": "ecs/fargateapp/1e1698469422439ea356071e581e8545"
},
"ContainerARN": "arn:aws:ecs:us-west-2:123456789012:container/testcluster/1e1698469422439ea356071e581e8545/050256a5-a7f3-461c-a16f-aca4eae37b01",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"10.10.10.10"
],
"AttachmentIndex": 0,
"MACAddress": "06:d7:3f:49:1d:a7",
"IPv4SubnetCIDRBlock": "10.10.10.0/20",
"DomainNameServers": [
"10.10.10.2"
],
"DomainNameSearchList": [
"us-west-2.compute.internal"
],
"PrivateDNSName": "ip-10-10-10-10.us-west-2.compute.internal",
"SubnetGatewayIpv4Address": "10.10.10.1/20"
}
],
"Snapshotter": "overlayfs"
}
""");

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, isLinux);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo(dockerId));
}

[TestCase(true)]
[TestCase(false)]
public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV3_IfUnableToParseV1OrV2(bool isLinux)
{
// This docker ID is in the Fargate format, but the test is still valid for non-Fargate ECS hosts.
var dockerId = "1e1698469422439ea356071e581e8545-2769485393";
SetEnvironmentVariable(AwsEcsMetadataV3EnvVar, $"http://169.254.170.2/v3/{dockerId}", EnvironmentVariableTarget.Process);
Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.IsAny<Uri>(), Arg.AnyString, Arg.AnyString, Arg.IsNull<IEnumerable<string>>())).Returns("""
{
"DockerId": "1e1698469422439ea356071e581e8545-2769485393",
"Name": "fargateapp",
"DockerName": "fargateapp",
"Image": "123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest",
"ImageID": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd",
"Labels": {
"com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:123456789012:cluster/testcluster",
"com.amazonaws.ecs.container-name": "fargateapp",
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545",
"com.amazonaws.ecs.task-definition-family": "fargatetestapp",
"com.amazonaws.ecs.task-definition-version": "7"
},
"DesiredStatus": "RUNNING",
"KnownStatus": "RUNNING",
"Limits": {
"CPU": 2
},
"CreatedAt": "2024-04-25T17:38:31.073208914Z",
"StartedAt": "2024-04-25T17:38:31.073208914Z",
"Type": "NORMAL",
"Networks": [
{
"NetworkMode": "awsvpc",
"IPv4Addresses": [
"10.10.10.10"
]
}
]
}
""");

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, isLinux);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo(dockerId));
}

[TestCase(true)]
[TestCase(false)]
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2OrEcs(bool isLinux)
{
// Not setting the ECS_CONTAINER_METADATA_URI_V4 env var will cause the fargate check to be skipped.

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);
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, isLinux);
Assert.That(model, Is.Null);
}
#endif
Expand Down

0 comments on commit c018b8a

Please sign in to comment.