Skip to content

Commit c018b8a

Browse files
authored
feat: Add support for capturing container id from AWS ECS. (#2481)
1 parent 27d0ed0 commit c018b8a

File tree

2 files changed

+201
-31
lines changed

2 files changed

+201
-31
lines changed

src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs

+67-22
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,25 @@
1212
using System.Collections.Generic;
1313
using System.Linq;
1414
using System.Text.RegularExpressions;
15-
16-
#if NETSTANDARD2_0
1715
using System.IO;
18-
using System.Runtime.InteropServices;
19-
#endif
2016

2117
namespace NewRelic.Agent.Core.Utilization
2218
{
2319
public class VendorInfo
2420
{
2521
private const string ValidateMetadataRegex = @"^[a-zA-Z0-9-_. /]*$";
26-
#if NETSTANDARD2_0
2722
private const string ContainerIdV1Regex = @".*cpu.*([0-9a-f]{64})";
2823
private const string ContainerIdV2Regex = ".*/docker/containers/([0-9a-f]{64})/.*";
29-
#endif
24+
private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI";
25+
private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4";
3026

3127
private const string AwsName = @"aws";
3228
private const string AzureName = @"azure";
3329
private const string GcpName = @"gcp";
3430
private const string PcfName = @"pcf";
3531
private const string DockerName = @"docker";
3632
private const string KubernetesName = @"kubernetes";
33+
private const string EcsFargateName = @"ecs-fargate";
3734

3835
private readonly string AwsTokenUri = @"http://169.254.169.254/latest/api/token";
3936
private readonly string AwsMetadataUri = @"http://169.254.169.254/latest/dynamic/instance-identity/document";
@@ -96,16 +93,11 @@ public IDictionary<string, IVendorModel> GetVendors()
9693
// If Docker info is set to be checked, it must be checked for all vendors.
9794
if (_configuration.UtilizationDetectDocker)
9895
{
99-
#if NETSTANDARD2_0
100-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
96+
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper(), IsLinux());
97+
if (dockerVendorInfo != null)
10198
{
102-
var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper());
103-
if (dockerVendorInfo != null)
104-
{
105-
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
106-
}
99+
vendors.Add(dockerVendorInfo.VendorName, dockerVendorInfo);
107100
}
108-
#endif
109101
}
110102

111103
if (_configuration.UtilizationDetectKubernetes)
@@ -276,10 +268,11 @@ private string GetProcessEnvironmentVariable(string variableName)
276268
}
277269
}
278270

279-
#if NETSTANDARD2_0
280-
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
271+
public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper, bool isLinux)
281272
{
282-
IVendorModel vendorModel = null;
273+
IVendorModel vendorModel = null;
274+
if (isLinux)
275+
{
283276
try
284277
{
285278
var fileContent = fileReaderWrapper.ReadAllText("/proc/self/mountinfo");
@@ -305,11 +298,47 @@ public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper)
305298
catch (Exception ex)
306299
{
307300
Log.Finest(ex, "Failed to parse Docker container id from /proc/self/cgroup.");
308-
return null;
309301
}
310302
}
303+
}
311304

312-
return vendorModel;
305+
if (vendorModel == null)
306+
{
307+
try
308+
{
309+
var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV4EnvVar);
310+
if (!string.IsNullOrWhiteSpace(metadataUri))
311+
{
312+
vendorModel = TryGetEcsFargateDockerId(metadataUri);
313+
if (vendorModel == null)
314+
Log.Finest($"Found {AwsEcsMetadataV4EnvVar} but failed to parse Docker container id.");
315+
}
316+
}
317+
catch (Exception ex)
318+
{
319+
Log.Finest(ex, $"Failed to parse Docker container id from {AwsEcsMetadataV4EnvVar}.");
320+
}
321+
}
322+
323+
if (vendorModel == null)
324+
{
325+
try
326+
{
327+
var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV3EnvVar);
328+
if (!string.IsNullOrWhiteSpace(metadataUri))
329+
{
330+
vendorModel = TryGetEcsFargateDockerId(metadataUri);
331+
if (vendorModel == null)
332+
Log.Finest($"Found {AwsEcsMetadataV3EnvVar} but failed to parse Docker container id.");
333+
}
334+
}
335+
catch (Exception ex)
336+
{
337+
Log.Finest(ex, $"Failed to parse Docker container id from {AwsEcsMetadataV3EnvVar}.");
338+
}
339+
}
340+
341+
return vendorModel;
313342
}
314343

315344
private IVendorModel TryGetDockerCGroupV1(string fileContent)
@@ -353,7 +382,15 @@ private IVendorModel TryGetDockerCGroupV2(string fileContent)
353382

354383
return id == null ? null : new DockerVendorModel(id);
355384
}
356-
#endif
385+
386+
private IVendorModel TryGetEcsFargateDockerId(string metadataUri)
387+
{
388+
var responseJson = _vendorHttpApiRequestor.CallVendorApi(new Uri(metadataUri), GetMethod, EcsFargateName);
389+
var jObject = JObject.Parse(responseJson);
390+
var idToken = jObject.SelectToken("DockerId");
391+
var id = NormalizeAndValidateMetadata((string)idToken, "DockerId", EcsFargateName);
392+
return id == null ? null : new DockerVendorModel(id);
393+
}
357394

358395
public IVendorModel GetKubernetesInfo()
359396
{
@@ -413,8 +450,17 @@ public bool IsValidMetadata(string data)
413450
{
414451
return Regex.IsMatch(data, ValidateMetadataRegex);
415452
}
416-
}
453+
454+
private static bool IsLinux()
455+
{
417456
#if NETSTANDARD2_0
457+
return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux);
458+
#else
459+
return false; // No Linux on .NET Framework
460+
#endif
461+
}
462+
}
463+
418464
// needed for unit testing only
419465
public interface IFileReaderWrapper
420466
{
@@ -428,5 +474,4 @@ public string ReadAllText(string fileName)
428474
return File.ReadAllText(fileName);
429475
}
430476
}
431-
#endif
432477
}

tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs

+134-9
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
7-
using NewRelic.Agent.Core.AgentHealth;
88
using NewRelic.Agent.Configuration;
9-
using NewRelic.Agent.TestUtilities;
9+
using NewRelic.Agent.Core.AgentHealth;
1010
using NewRelic.SystemInterfaces;
1111
using NUnit.Framework;
1212
using Telerik.JustMock;
@@ -27,6 +27,8 @@ public class VendorInfoTests
2727
private const string PcfInstanceIp = @"CF_INSTANCE_IP";
2828
private const string PcfMemoryLimit = @"MEMORY_LIMIT";
2929
private const string KubernetesServiceHost = @"KUBERNETES_SERVICE_HOST";
30+
private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI";
31+
private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4";
3032

3133
[SetUp]
3234
public void Setup()
@@ -338,7 +340,7 @@ public void GetVendors_GetDockerVendorInfo_ParsesV2()
338340
1342 1429 0:300 / /sys/firmware ro,relatime - tmpfs tmpfs ro
339341
");
340342

341-
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
343+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
342344
Assert.That(model, Is.Not.Null);
343345
Assert.That(model.Id, Is.EqualTo("adf04870aa0a9f01fb712e283765ee5d7c7b1c1c0ad8ebfdea20a8bb3ae382fb"));
344346
}
@@ -367,7 +369,7 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfV2LookupFailsToParseFile()
367369
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
368370
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");
369371

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

435-
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
437+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
436438
Assert.That(model, Is.Not.Null);
437439
Assert.That(model.Id, Is.EqualTo("b10c13eeeea82c495c9e2fbb07ab448024715fdd55218e22cce6cd815c84bd58"));
438440
}
@@ -462,20 +464,143 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist()
462464
1:cpuset:/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043
463465
0::/docker/b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043");
464466

465-
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
467+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, true);
466468
Assert.That(model, Is.Not.Null);
467469
Assert.That(model.Id, Is.EqualTo("b9d734e13dc5f508571d975edade94a05dfc637e73a83e11077a39bc11681043"));
468470
}
469471

470-
[Test]
471-
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2()
472+
[TestCase(true)]
473+
[TestCase(false)]
474+
public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV4_IfUnableToParseV1OrV2(bool isLinux)
475+
{
476+
// This docker ID is in the Fargate format, but the test is still valid for non-Fargate ECS hosts.
477+
var dockerId = "1e1698469422439ea356071e581e8545-2769485393";
478+
SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, $"http://169.254.170.2/v4/{dockerId}", EnvironmentVariableTarget.Process);
479+
Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.IsAny<Uri>(), Arg.AnyString, Arg.AnyString, Arg.IsNull<IEnumerable<string>>())).Returns("""
480+
{
481+
"DockerId": "1e1698469422439ea356071e581e8545-2769485393",
482+
"Name": "fargateapp",
483+
"DockerName": "fargateapp",
484+
"Image": "123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest",
485+
"ImageID": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd",
486+
"Labels": {
487+
"com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:123456789012:cluster/testcluster",
488+
"com.amazonaws.ecs.container-name": "fargateapp",
489+
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545",
490+
"com.amazonaws.ecs.task-definition-family": "fargatetestapp",
491+
"com.amazonaws.ecs.task-definition-version": "7"
492+
},
493+
"DesiredStatus": "RUNNING",
494+
"KnownStatus": "RUNNING",
495+
"Limits": {
496+
"CPU": 2
497+
},
498+
"CreatedAt": "2024-04-25T17:38:31.073208914Z",
499+
"StartedAt": "2024-04-25T17:38:31.073208914Z",
500+
"Type": "NORMAL",
501+
"LogDriver": "awslogs",
502+
"LogOptions": {
503+
"awslogs-create-group": "true",
504+
"awslogs-group": "/ecs/fargatetestapp",
505+
"awslogs-region": "us-west-2",
506+
"awslogs-stream": "ecs/fargateapp/1e1698469422439ea356071e581e8545"
507+
},
508+
"ContainerARN": "arn:aws:ecs:us-west-2:123456789012:container/testcluster/1e1698469422439ea356071e581e8545/050256a5-a7f3-461c-a16f-aca4eae37b01",
509+
"Networks": [
510+
{
511+
"NetworkMode": "awsvpc",
512+
"IPv4Addresses": [
513+
"10.10.10.10"
514+
],
515+
"AttachmentIndex": 0,
516+
"MACAddress": "06:d7:3f:49:1d:a7",
517+
"IPv4SubnetCIDRBlock": "10.10.10.0/20",
518+
"DomainNameServers": [
519+
"10.10.10.2"
520+
],
521+
"DomainNameSearchList": [
522+
"us-west-2.compute.internal"
523+
],
524+
"PrivateDNSName": "ip-10-10-10-10.us-west-2.compute.internal",
525+
"SubnetGatewayIpv4Address": "10.10.10.1/20"
526+
}
527+
],
528+
"Snapshotter": "overlayfs"
529+
}
530+
""");
531+
532+
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
533+
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
534+
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("blah blah blah");
535+
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns("foo bar baz");
536+
537+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, isLinux);
538+
Assert.That(model, Is.Not.Null);
539+
Assert.That(model.Id, Is.EqualTo(dockerId));
540+
}
541+
542+
[TestCase(true)]
543+
[TestCase(false)]
544+
public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV3_IfUnableToParseV1OrV2(bool isLinux)
545+
{
546+
// This docker ID is in the Fargate format, but the test is still valid for non-Fargate ECS hosts.
547+
var dockerId = "1e1698469422439ea356071e581e8545-2769485393";
548+
SetEnvironmentVariable(AwsEcsMetadataV3EnvVar, $"http://169.254.170.2/v3/{dockerId}", EnvironmentVariableTarget.Process);
549+
Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.IsAny<Uri>(), Arg.AnyString, Arg.AnyString, Arg.IsNull<IEnumerable<string>>())).Returns("""
550+
{
551+
"DockerId": "1e1698469422439ea356071e581e8545-2769485393",
552+
"Name": "fargateapp",
553+
"DockerName": "fargateapp",
554+
"Image": "123456789012.dkr.ecr.us-west-2.amazonaws.com/fargatetest:latest",
555+
"ImageID": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd",
556+
"Labels": {
557+
"com.amazonaws.ecs.cluster": "arn:aws:ecs:us-west-2:123456789012:cluster/testcluster",
558+
"com.amazonaws.ecs.container-name": "fargateapp",
559+
"com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:123456789012:task/testcluster/1e1698469422439ea356071e581e8545",
560+
"com.amazonaws.ecs.task-definition-family": "fargatetestapp",
561+
"com.amazonaws.ecs.task-definition-version": "7"
562+
},
563+
"DesiredStatus": "RUNNING",
564+
"KnownStatus": "RUNNING",
565+
"Limits": {
566+
"CPU": 2
567+
},
568+
"CreatedAt": "2024-04-25T17:38:31.073208914Z",
569+
"StartedAt": "2024-04-25T17:38:31.073208914Z",
570+
"Type": "NORMAL",
571+
"Networks": [
572+
{
573+
"NetworkMode": "awsvpc",
574+
"IPv4Addresses": [
575+
"10.10.10.10"
576+
]
577+
}
578+
]
579+
}
580+
""");
581+
582+
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
583+
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
584+
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("blah blah blah");
585+
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns("foo bar baz");
586+
587+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, isLinux);
588+
Assert.That(model, Is.Not.Null);
589+
Assert.That(model.Id, Is.EqualTo(dockerId));
590+
}
591+
592+
[TestCase(true)]
593+
[TestCase(false)]
594+
public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2OrEcs(bool isLinux)
472595
{
596+
// Not setting the ECS_CONTAINER_METADATA_URI_V4 env var will cause the fargate check to be skipped.
597+
473598
var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor);
474599
var mockFileReaderWrapper = Mock.Create<IFileReaderWrapper>();
475600
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("blah blah blah");
476601
Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns("foo bar baz");
477602

478-
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper);
603+
var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, isLinux);
479604
Assert.That(model, Is.Null);
480605
}
481606
#endif

0 commit comments

Comments
 (0)