Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,74 @@ public ResourceOutgoingPeerResolver(IDashboardClient resourceService)
}

public bool TryResolvePeerName(KeyValuePair<string, string>[] attributes, [NotNullWhen(true)] out string? name)
{
return TryResolvePeerNameCore(_resourceByName, attributes, out name);
}

internal static bool TryResolvePeerNameCore(IDictionary<string, ResourceViewModel> resources, KeyValuePair<string, string>[] attributes, out string? name)
{
var address = OtlpHelpers.GetValue(attributes, OtlpSpan.PeerServiceAttributeKey);
if (address != null)
{
foreach (var (resourceName, resource) in _resourceByName)
// Match exact value.
if (TryMatchResourceAddress(address, out name))
{
return true;
}

// Resource addresses have the format "127.0.0.1:5000". Some libraries modify the peer.service value on the span.
// If there isn't an exact match then transform the peer.service value and try to match again.
// Change from transformers are cumulative. e.g. "localhost,5000" -> "localhost:5000" -> "127.0.0.1:5000"
var transformedAddress = address;
foreach (var transformer in s_addressTransformers)
{
transformedAddress = transformer(transformedAddress);
if (TryMatchResourceAddress(transformedAddress, out name))
{
return true;
}
}
}

name = null;
return false;

bool TryMatchResourceAddress(string value, [NotNullWhen(true)] out string? name)
{
foreach (var (resourceName, resource) in resources)
{
foreach (var service in resource.Services)
{
if (string.Equals(service.AddressAndPort, address, StringComparison.OrdinalIgnoreCase))
if (string.Equals(service.AddressAndPort, value, StringComparison.OrdinalIgnoreCase))
{
name = resource.Name;
return true;
}
}
}
}

name = null;
return false;
name = null;
return false;
}
}

private static readonly List<Func<string, string>> s_addressTransformers = [
s =>
{
// SQL Server uses comma instead of colon for port.
// https://www.connectionstrings.com/sql-server/
if (s.AsSpan().Count(',') == 1)
{
return s.Replace(',', ':');
}
return s;
},
s =>
{
// Some libraries use "127.0.0.1" instead of "localhost".
return s.Replace("127.0.0.1:", "localhost:");
}];

public IDisposable OnPeerChanges(Func<Task> callback)
{
lock (_lock)
Expand Down
116 changes: 116 additions & 0 deletions tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Frozen;
using System.Collections.Immutable;
using Aspire.Dashboard.Model;
using Google.Protobuf.WellKnownTypes;
using Xunit;

namespace Aspire.Dashboard.Tests;

public class ResourceOutgoingPeerResolverTests
{
private static ResourceViewModel CreateResource(string name, string? serviceAddress = null, int? servicePort = null)
{
ImmutableArray<ResourceServiceViewModel> resourceServices = serviceAddress is null || servicePort is null
? ImmutableArray<ResourceServiceViewModel>.Empty
: [new ResourceServiceViewModel("http", serviceAddress, servicePort)];

return new ResourceViewModel
{
Name = name,
ResourceType = "Container",
DisplayName = name,
Uid = Guid.NewGuid().ToString(),
CreationTimeStamp = DateTime.UtcNow,
Environment = ImmutableArray<EnvironmentVariableViewModel>.Empty,
Endpoints = ImmutableArray<EndpointViewModel>.Empty,
Services = resourceServices,
ExpectedEndpointsCount = 0,
Properties = FrozenDictionary<string, Value>.Empty,
State = null
};
}

[Fact]
public void EmptyAttributes_NoMatch()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.False(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [], out _));
}

[Fact]
public void EmptyUrlAttribute_NoMatch()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.False(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [KeyValuePair.Create("peer.service", "")], out _));
}

[Fact]
public void NullUrlAttribute_NoMatch()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.False(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [KeyValuePair.Create<string, string>("peer.service", null!)], out _));
}

[Fact]
public void ExactValueAttribute_Match()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.True(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [KeyValuePair.Create("peer.service", "localhost:5000")], out var value));
Assert.Equal("test", value);
}

[Fact]
public void NumberAddressValueAttribute_Match()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.True(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [KeyValuePair.Create("peer.service", "127.0.0.1:5000")], out var value));
Assert.Equal("test", value);
}

[Fact]
public void CommaAddressValueAttribute_Match()
{
// Arrange
var resources = new Dictionary<string, ResourceViewModel>
{
["test"] = CreateResource("test", "localhost", 5000)
};

// Act & Assert
Assert.True(ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, [KeyValuePair.Create("peer.service", "127.0.0.1,5000")], out var value));
Assert.Equal("test", value);
}
}