Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for capturing container id from AWS ECS. #2481

Merged
merged 9 commits into from
Jun 12, 2024
68 changes: 58 additions & 10 deletions src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class VendorInfo
#if NETSTANDARD2_0
private const string ContainerIdV1Regex = @".*cpu.*([0-9a-f]{64})";
private const string ContainerIdV2Regex = ".*/docker/containers/([0-9a-f]{64})/.*";
private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI";
private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4";
#endif

private const string AwsName = @"aws";
Expand All @@ -34,6 +36,7 @@ public class VendorInfo
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 @@ -97,13 +100,10 @@ public VendorInfo(IConfiguration configuration, IAgentHealthReporter agentHealth
if (_configuration.UtilizationDetectDocker)
{
#if NETSTANDARD2_0
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper(), RuntimeInformation.IsOSPlatform(OSPlatform.Linux));
nrcventura marked this conversation as resolved.
Show resolved Hide resolved
if (dockerVendorInfo != null)
{
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper());
if (dockerVendorInfo != null)
{
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
}
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
}
#endif
}
Expand Down Expand Up @@ -277,9 +277,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 +307,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,6 +391,16 @@ private IVendorModel TryGetDockerCGroupV2(string fileContent)

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

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);
}

#endif

public IVendorModel GetKubernetesInfo()
Expand Down
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
Loading