From d55567fb4674ea64443ac4a96e7d3f7f81dc3c97 Mon Sep 17 00:00:00 2001 From: Jacob Affinito Date: Wed, 18 Sep 2024 10:03:17 -0700 Subject: [PATCH] feat: Allow container linking for AWS ECS applications. (#2683) --- .../Agent/Core/Utilization/EcsVendorModel.cs | 22 ++ .../Agent/Core/Utilization/VendorInfo.cs | 116 +++--- .../Utilization/VendorInfoTests.cs | 360 ++++++++++++++++-- 3 files changed, 423 insertions(+), 75 deletions(-) create mode 100644 src/Agent/NewRelic/Agent/Core/Utilization/EcsVendorModel.cs diff --git a/src/Agent/NewRelic/Agent/Core/Utilization/EcsVendorModel.cs b/src/Agent/NewRelic/Agent/Core/Utilization/EcsVendorModel.cs new file mode 100644 index 000000000..c676dd3f7 --- /dev/null +++ b/src/Agent/NewRelic/Agent/Core/Utilization/EcsVendorModel.cs @@ -0,0 +1,22 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Newtonsoft.Json; + +namespace NewRelic.Agent.Core.Utilization +{ + public class EcsVendorModel : IVendorModel + { + private readonly string _ecsDockerId; + + public string VendorName { get { return "ecs"; } } + + [JsonProperty("ecsDockerId", NullValueHandling = NullValueHandling.Ignore)] + public string EcsDockerId { get { return _ecsDockerId; } } + + public EcsVendorModel(string ecsDockerId) + { + _ecsDockerId = ecsDockerId; + } + } +} diff --git a/src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs b/src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs index ed19983f2..379e09cdf 100644 --- a/src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs +++ b/src/Agent/NewRelic/Agent/Core/Utilization/VendorInfo.cs @@ -20,8 +20,9 @@ namespace NewRelic.Agent.Core.Utilization public class VendorInfo { private const string ValidateMetadataRegex = @"^[a-zA-Z0-9-_. /]*$"; - private const string ContainerIdV1Regex = @".*cpu.*([0-9a-f]{64})"; - private const string ContainerIdV2Regex = ".*/docker/containers/([0-9a-f]{64})/.*"; + private const string StandardDockerIdRegex = "([0-9a-f]{64})"; + private const string ContainerIdV1Regex = @".*cpu.*" + StandardDockerIdRegex; + private const string ContainerIdV2Regex = ".*/docker/containers/" + StandardDockerIdRegex + "/.*"; private const string AwsEcsMetadataV3EnvVar = "ECS_CONTAINER_METADATA_URI"; private const string AwsEcsMetadataV4EnvVar = "ECS_CONTAINER_METADATA_URI_V4"; @@ -32,7 +33,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 const string EcsName = @"ecs"; 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"; @@ -72,15 +73,33 @@ public IDictionary GetVendors() var vendorMethods = new List>(); if (_configuration.UtilizationDetectAws) + { vendorMethods.Add(GetAwsVendorInfo); + + // Directly add the ECS vendor info if AWS is enabled and not null. + var ecsVendorInfo = GetEcsVendorInfo(); + if (ecsVendorInfo != null) + { + vendors.Add(ecsVendorInfo.VendorName, ecsVendorInfo); + } + + } if (_configuration.UtilizationDetectAzure) + { vendorMethods.Add(GetAzureVendorInfo); + } if (_configuration.UtilizationDetectGcp) + { vendorMethods.Add(GetGcpVendorInfo); + } if (_configuration.UtilizationDetectPcf) + { vendorMethods.Add(GetPcfVendorInfo); + } if (_configuration.UtilizationDetectAzureFunction) + { vendorMethods.Add(GetAzureFunctionVendorInfo); + } foreach (var vendorMethod in vendorMethods) { @@ -94,8 +113,9 @@ public IDictionary GetVendors() } } - // If Docker info is set to be checked, it must be checked for all vendors. - if (_configuration.UtilizationDetectDocker) + // If Docker info is set to be checked, it must be checked for all vendors even disabled ones. + // If we get AWS ECS info, we don't need to check Docker. + if (_configuration.UtilizationDetectDocker && !vendors.ContainsKey(EcsName)) { var dockerVendorInfo = GetDockerVendorInfo(new FileReaderWrapper(), IsLinux()); if (dockerVendorInfo != null) @@ -322,42 +342,6 @@ public IVendorModel GetDockerVendorInfo(IFileReaderWrapper fileReaderWrapper, bo } } - 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; } @@ -403,13 +387,55 @@ private IVendorModel TryGetDockerCGroupV2(string fileContent) return id == null ? null : new DockerVendorModel(id); } - private IVendorModel TryGetEcsFargateDockerId(string metadataUri) + public IVendorModel GetEcsVendorInfo() { - var responseJson = _vendorHttpApiRequestor.CallVendorApi(new Uri(metadataUri), GetMethod, EcsFargateName); + IVendorModel ecsVendorModel = null; + try + { + var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV4EnvVar); + if (!string.IsNullOrWhiteSpace(metadataUri)) + { + ecsVendorModel = TryGetEcsVendorModel(metadataUri); + if (ecsVendorModel == 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 (ecsVendorModel == null) + { + try + { + var metadataUri = GetProcessEnvironmentVariable(AwsEcsMetadataV3EnvVar); + if (!string.IsNullOrWhiteSpace(metadataUri)) + { + ecsVendorModel = TryGetEcsVendorModel(metadataUri); + if (ecsVendorModel == 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 ecsVendorModel; + } + + private IVendorModel TryGetEcsVendorModel(string metadataUri) + { + var responseJson = _vendorHttpApiRequestor.CallVendorApi(new Uri(metadataUri), GetMethod, EcsName); var jObject = JObject.Parse(responseJson); var idToken = jObject.SelectToken("DockerId"); - var id = NormalizeAndValidateMetadata((string)idToken, "DockerId", EcsFargateName); - return id == null ? null : new DockerVendorModel(id); + return idToken == null ? null : new EcsVendorModel((string)idToken); } public IVendorModel GetKubernetesInfo() diff --git a/tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs index b33265562..b1a3af3d1 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Utilization/VendorInfoTests.cs @@ -10,6 +10,7 @@ using NewRelic.Agent.Core.SharedInterfaces; using NUnit.Framework; using Telerik.JustMock; +using Telerik.JustMock.Helpers; namespace NewRelic.Agent.Core.Utilization @@ -440,7 +441,6 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_ForCustomerIssue() Assert.That(model.Id, Is.EqualTo("b10c13eeeea82c495c9e2fbb07ab448024715fdd55218e22cce6cd815c84bd58")); } - [Test] public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist() { @@ -472,12 +472,25 @@ public void GetVendors_GetDockerVendorInfo_ParsesV1_IfMountinfoDoesNotExist() [TestCase(true)] [TestCase(false)] - public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV4_IfUnableToParseV1OrV2(bool isLinux) + public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2(bool isLinux) + { + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + var mockFileReaderWrapper = Mock.Create(); + 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.Null); + } +#endif + + [Test] + public void GetVendors_CapturesEcs_WhenAwsExists() { // 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(), Arg.AnyString, Arg.AnyString, Arg.IsNull>())).Returns(""" + var ecsUri = $"http://169.254.170.2/v4/{dockerId}"; + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.Matches(u => u.OriginalString == ecsUri), Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns(""" { "DockerId": "1e1698469422439ea356071e581e8545-2769485393", "Name": "fargateapp", @@ -530,24 +543,258 @@ public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV4_IfUnableToParseV1OrV2 } """); + var awsTokenUri = @"http://169.254.169.254/latest/api/token"; + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.Matches(u => u.OriginalString == awsTokenUri), Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns("token"); + + var instanceId = "i-1234567890abcdef0"; + var awsMetadataUri = @"http://169.254.169.254/latest/dynamic/instance-identity/document"; + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.Matches(u => u.OriginalString == awsMetadataUri), Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns(""" +{ + "availabilityZone" : "us - east - 1d", + "instanceId" : "i-1234567890abcdef0", + "instanceType" : "t1.micro" +} +"""); + + SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, ecsUri, EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); - var mockFileReaderWrapper = Mock.Create(); - Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/mountinfo")).Returns("blah blah blah"); - Mock.Arrange(() => mockFileReaderWrapper.ReadAllText("/proc/self/cgroup")).Returns("foo bar baz"); + var vendors = vendorInfo.GetVendors(); - var model = (DockerVendorModel)vendorInfo.GetDockerVendorInfo(mockFileReaderWrapper, isLinux); + var ecsModel = vendors["ecs"]; + var awsModel = vendors["aws"]; + Assert.That(ecsModel, Is.Not.Null); + Assert.That(awsModel, Is.Not.Null); + Assert.That(((EcsVendorModel)ecsModel).EcsDockerId.Equals(dockerId)); + Assert.That(((AwsVendorModel)awsModel).InstanceId.Equals(instanceId)); + } + + [Test] + public void GetVendors_CapturesEcs_WhenAwsIsNull() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsNull>())).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" +} +"""); + + // 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(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + var vendors = vendorInfo.GetVendors(); + + var model = vendors["ecs"]; Assert.That(model, Is.Not.Null); - Assert.That(model.Id, Is.EqualTo(dockerId)); + Assert.That(((EcsVendorModel)model).EcsDockerId.Equals(dockerId)); } - [TestCase(true)] - [TestCase(false)] - public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV3_IfUnableToParseV1OrV2(bool isLinux) + [Test] + public void GetVendors_DoesNotCaptureDocker_WhenEcsExists() { // 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(), Arg.AnyString, Arg.AnyString, Arg.IsNull>())).Returns(""" + var ecsUri = $"http://169.254.170.2/v4/{dockerId}"; + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.Matches(u => u.OriginalString == ecsUri), Arg.AnyString, Arg.AnyString, Arg.IsAny>())).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" +} +"""); + + SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, ecsUri, EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); + Mock.Arrange(() => _configuration.UtilizationDetectDocker).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + var vendors = vendorInfo.GetVendors(); + + var ecsModel = vendors["ecs"]; + + Assert.That(vendors, Does.Not.ContainKey("docker")); + Assert.That(ecsModel, Is.Not.Null); + Assert.That(((EcsVendorModel)ecsModel).EcsDockerId.Equals(dockerId)); + } + + [Test] + public void GetVendors_Vendors_IsEmpty_BadEcsMetadata() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns("{ awesome: 0 }"); + + // 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(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + var vendors = vendorInfo.GetVendors(); + + Assert.That(vendors, Is.Empty); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_VarV4() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).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" +} +"""); + + // 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(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + + var model = vendorInfo.GetEcsVendorInfo(); + Assert.That(model, Is.Not.Null); + Assert.That(((EcsVendorModel)model).EcsDockerId.Equals(dockerId)); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_VarV3() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns(""" { "DockerId": "1e1698469422439ea356071e581e8545-2769485393", "Name": "fargateapp", @@ -580,31 +827,86 @@ public void GetVendors_GetDockerVendorInfo_ParsesEcs_VarV3_IfUnableToParseV1OrV2 } """); + // 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(() => _configuration.UtilizationDetectAws).Returns(true); var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); - var mockFileReaderWrapper = Mock.Create(); - 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); + var model = vendorInfo.GetEcsVendorInfo(); Assert.That(model, Is.Not.Null); - Assert.That(model.Id, Is.EqualTo(dockerId)); + Assert.That(((EcsVendorModel)model).EcsDockerId.Equals(dockerId)); } - [TestCase(true)] - [TestCase(false)] - public void GetVendors_GetDockerVendorInfo_ReturnsNull_IfUnableToParseV1OrV2OrEcs(bool isLinux) + [Test] + public void GetVendors_GetEcsVendorInfo_Throws_VarV4() { - // Not setting the ECS_CONTAINER_METADATA_URI_V4 env var will cause the fargate check to be skipped. + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Throws(new Exception()); + // This docker ID is in the ec2 format + var dockerId = "ae4c507ab5956a9dee9b908e221d72616373861d7ccc3c9703aa346571aef9ef"; + SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, $"http://169.254.170.2/v4/{dockerId}", EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); - var mockFileReaderWrapper = Mock.Create(); - 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); + var model = vendorInfo.GetEcsVendorInfo(); + Assert.That(model, Is.Null); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_Throws_VarV3() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Throws(new Exception()); + + // This docker ID is in the ec2 format + var dockerId = "ae4c507ab5956a9dee9b908e221d72616373861d7ccc3c9703aa346571aef9ef"; + SetEnvironmentVariable(AwsEcsMetadataV3EnvVar, $"http://169.254.170.2/v3/{dockerId}", EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + + var model = vendorInfo.GetEcsVendorInfo(); + Assert.That(model, Is.Null); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_BadMetadata_VarV4() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns("{ awesome: 0 }"); + + // This docker ID is in the ec2 format + var dockerId = "ae4c507ab5956a9dee9b908e221d72616373861d7ccc3c9703aa346571aef9ef"; + SetEnvironmentVariable(AwsEcsMetadataV4EnvVar, $"http://169.254.170.2/v4/{dockerId}", EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + + var model = vendorInfo.GetEcsVendorInfo(); + Assert.That(model, Is.Null); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_BadMetadata_VarV3() + { + Mock.Arrange(() => _vendorHttpApiRequestor.CallVendorApi(Arg.AnyUri, Arg.AnyString, Arg.AnyString, Arg.IsAny>())).Returns("{ awesome: 0 }"); + + // This docker ID is in the ec2 format + var dockerId = "ae4c507ab5956a9dee9b908e221d72616373861d7ccc3c9703aa346571aef9ef"; + SetEnvironmentVariable(AwsEcsMetadataV3EnvVar, $"http://169.254.170.2/v3/{dockerId}", EnvironmentVariableTarget.Process); + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + + var model = vendorInfo.GetEcsVendorInfo(); + Assert.That(model, Is.Null); + } + + [Test] + public void GetVendors_GetEcsVendorInfo_Returns_Null() + { + Mock.Arrange(() => _configuration.UtilizationDetectAws).Returns(true); + var vendorInfo = new VendorInfo(_configuration, _agentHealthReporter, _environment, _vendorHttpApiRequestor); + + var model = vendorInfo.GetEcsVendorInfo(); Assert.That(model, Is.Null); } -#endif [Test] [TestCase(true)] @@ -685,8 +987,6 @@ public void GetAzureFunctionVendorInfo_ReturnsNull_WhenResourceIdIsNotAvailable( Assert.That(model, Is.Null); } - - private void SetEnvironmentVariable(string variableName, string value, EnvironmentVariableTarget environmentVariableTarget) { Mock.Arrange(() => _environment.GetEnvironmentVariable(variableName, environmentVariableTarget)).Returns(value);