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
74 changes: 63 additions & 11 deletions src/Aspire.Hosting.Yarp/ConfigurationBuilder/YarpCluster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ public class YarpCluster
internal YarpCluster(ClusterConfig config, params object[] targets)
{
ClusterConfig = config;
Targets = targets;
_targets = targets;
}

private readonly object[] _targets;

/// <summary>
/// Construct a new YarpCluster targeting the endpoint in parameter.
/// </summary>
/// <param name="endpoint">The endpoint to target.</param>
internal YarpCluster(EndpointReference endpoint)
: this(endpoint.Resource.Name, BuildEndpointTarget(endpoint))
: this(endpoint.Resource.Name, endpoint)
{
}

Expand All @@ -48,7 +51,7 @@ private static string BuildEndpointTarget(EndpointReference endpoint)
/// </summary>
/// <param name="resource">The resource to target.</param>
internal YarpCluster(IResourceWithServiceDiscovery resource)
: this(resource.Name, BuildEndpointUri(resource))
: this(resource.Name, resource)
{
}

Expand All @@ -57,7 +60,7 @@ internal YarpCluster(IResourceWithServiceDiscovery resource)
/// </summary>
/// <param name="externalService">The external service.</param>
internal YarpCluster(ExternalServiceResource externalService)
: this(externalService.Name, GetAddressFromExternalService(externalService))
: this(externalService.Name, externalService)
{
}

Expand All @@ -73,25 +76,74 @@ internal YarpCluster(string resourceName, params object[] targets)
ClusterId = $"cluster_{resourceName}",
Destinations = new Dictionary<string, DestinationConfig>(StringComparer.OrdinalIgnoreCase)
};
Targets = targets;
_targets = targets;
}

internal ClusterConfig ClusterConfig { get; private set; }

internal object[] Targets { get; private set; }
internal object[] Targets => GetResolvedTargets();

internal void Configure(Func<ClusterConfig, ClusterConfig> configure)
{
ClusterConfig = configure(ClusterConfig);
}

private static object BuildEndpointUri(IResourceWithServiceDiscovery resource)
internal object[] GetResolvedTargets()
{
var targets = new List<object>(_targets.Length);
foreach (var target in _targets)
{
targets.AddRange(ResolveTargets(target));
}

return [.. targets];
}

private static IEnumerable<object> ResolveTargets(object target)
{
switch (target)
{
case EndpointReference endpoint:
yield return BuildEndpointTarget(endpoint);
break;
case IResourceWithServiceDiscovery resource:
foreach (var resourceTarget in BuildEndpointTargets(resource))
{
yield return resourceTarget;
}
break;
case ExternalServiceResource externalService:
yield return GetAddressFromExternalService(externalService);
break;
default:
yield return target;
break;
}
}

private static object[] BuildEndpointTargets(IResourceWithServiceDiscovery resource)
{
var resourceName = resource.Name;

var endpoints = resource.GetEndpoints();
var hasHttpsEndpoint = endpoints.Any(e => e.Exists && e.IsHttps);
var hasHttpEndpoint = endpoints.Any(e => e.Exists && e.IsHttp);
var endpoints = resource.GetEndpoints()
.Where(e => e.Exists && !e.ExcludeReferenceEndpoint && (e.IsHttp || e.IsHttps))
.ToArray();
var schemeNamedEndpoints = endpoints
.Where(e => e.IsHttpSchemeNamedEndpoint)
.ToArray();

if (schemeNamedEndpoints.Length == 0)
{
if (endpoints.Length == 0)
{
throw new ArgumentException("Cannot find a http or https endpoint for this resource.", nameof(resource));
}

return [.. endpoints.Select(BuildEndpointTarget)];
}

var hasHttpsEndpoint = schemeNamedEndpoints.Any(e => e.IsHttps);
var hasHttpEndpoint = schemeNamedEndpoints.Any(e => e.IsHttp);

var scheme = (hasHttpsEndpoint, hasHttpEndpoint) switch
{
Expand All @@ -101,7 +153,7 @@ private static object BuildEndpointUri(IResourceWithServiceDiscovery resource)
_ => throw new ArgumentException("Cannot find a http or https endpoint for this resource.", nameof(resource))
};

return $"{scheme}://{resourceName}";
return [$"{scheme}://{resourceName}"];
}

private static object GetAddressFromExternalService(ExternalServiceResource externalService)
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Hosting.Yarp/YarpEnvConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ public static void PopulateEnvVariables(Dictionary<string, object> environmentVa
foreach (var cluster in clusters)
{
FlattenToEnvVars(environmentVariables, cluster.ClusterConfig, $"{Prefix}CLUSTERS__{cluster.ClusterConfig.ClusterId}");
for (var i =0; i < cluster.Targets.Length; i++)
var targets = cluster.GetResolvedTargets();
for (var i = 0; i < targets.Length; i++)
{
environmentVariables[$"{Prefix}CLUSTERS__{cluster.ClusterConfig.ClusterId}__DESTINATIONS__destination{i + 1}__ADDRESS"] = cluster.Targets[i];
environmentVariables[$"{Prefix}CLUSTERS__{cluster.ClusterConfig.ClusterId}__DESTINATIONS__destination{i + 1}__ADDRESS"] = targets[i];
}
// Hack: YARP throws if ClusterConfig.ClusterId is populated in the config.
// YARP will get the ClusterId from the config key and populate the value in the ClusterConfig itself.
Expand Down
30 changes: 30 additions & 0 deletions tests/Aspire.Hosting.Yarp.Tests/AddYarpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ public async Task VerifyPublishEnvVariablesAreSet()
Assert.DoesNotContain("YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE", env);
}

[Fact]
public void VerifyResourceRouteDestinationUsesLatestEndpointScheme()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);

var backend = builder.AddResource(new TestServiceDiscoveryResource("backend"))
.WithHttpEndpoint();

var yarp = builder.AddYarp("yarp")
.WithConfiguration(config =>
{
config.AddRoute("/api/{**catchall}", backend);
});

backend.WithEndpoint("http", endpoint => endpoint.UriScheme = "https");

var env = new Dictionary<string, object>();
YarpEnvConfigGenerator.PopulateEnvVariables(env, yarp.Resource.Routes, yarp.Resource.Clusters);

var address = Assert.Contains("REVERSEPROXY__CLUSTERS__cluster_backend__DESTINATIONS__destination1__ADDRESS", env);
Assert.Equal("https://backend", address);
}

[Fact]
public async Task VerifyWithStaticFilesAddsEnvironmentVariable()
{
Expand Down Expand Up @@ -451,4 +474,11 @@ public async Task VerifyWithHostHttpsPortDoesNotCreateHttpsEndpointWithoutCertif
private sealed class TestContainerFilesResource(string name) : ContainerResource(name), IResourceWithContainerFiles
{
}

private sealed class TestServiceDiscoveryResource(string name) : IResourceWithServiceDiscovery
{
public string Name => name;

public ResourceAnnotationCollection Annotations { get; } = new ResourceAnnotationCollection();
}
}
12 changes: 12 additions & 0 deletions tests/Aspire.Hosting.Yarp.Tests/YarpClusterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ public void Create_YarpCluster_From_Resource_With_One_Endpoint()
Assert.Equal($"https://ServiceD", httpsCluster.Targets[0]);
}

[Fact]
public void Create_YarpCluster_From_Resource_With_Named_Endpoint()
{
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
var service = builder.AddResource(new TestResource("ServiceC"))
.WithEndpoint(name: "api", scheme: "http");

var cluster = new YarpCluster(service.Resource);

Assert.Equal("http://_api.ServiceC", Assert.Single(cluster.Targets));
}

[Fact]
public void Create_YarpCluster_From_Resource_With_Both_Endpoints()
{
Expand Down
Loading