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
78 changes: 63 additions & 15 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 @@ -279,37 +282,72 @@ private string GetProcessEnvironmentVariable(string variableName)
#if NETSTANDARD2_0
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
{
IVendorModel vendorModel = null;
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/mountinfo");
vendorModel = TryGetDockerCGroupV2(fileContent);
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/cgroup");
vendorModel = TryGetDockerCGroupV1(fileContent);
if (vendorModel == null)
Log.Finest("Found /proc/self/mountinfo but failed to parse Docker container id.");

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/mountinfo.");
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/cgroup.");
}
}

if (vendorModel == null) // fall back to the v1 check if v2 wasn't successful
if (vendorModel == null)
{
try
{
try
var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV4EnvVar);
if (!string.IsNullOrWhiteSpace(metadataUri))
{
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/cgroup");
vendorModel = TryGetDockerCGroupV1(fileContent);
vendorModel = TryGetEcsFargateDockerId(metadataUri);
if (vendorModel == null)
Log.Finest("Found /proc/self/cgroup but failed to parse Docker container id.");
Log.Finest($"Found {AwsEcsMetadataV4EnvVar} but failed to parse Docker container id.");
}
catch (Exception ex)
}
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))
{
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/cgroup.");
return null;
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;
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
126 changes: 123 additions & 3 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 @@ -468,8 +470,126 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist()
}

[Test]
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2()
public void GetVendors_GetDockerVendorInfo_ParsesEcsFargate_VarV4_IfUnableToParseV1OrV2()
{
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);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo(dockerId));
}

[Test]
public void GetVendors_GetDockerVendorInfo_ParsesEcsFargate_VarV3_IfUnableToParseV1OrV2()
{
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);
Assert.That(model, Is.Not.Null);
Assert.That(model.Id, Is.EqualTo(dockerId));
}

[Test]
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2OrEcsFargate()
{
// 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");
Expand Down
Loading