Skip to content

Commit

Permalink
Handle parsing wildcard urls in launch profiles (#5588)
Browse files Browse the repository at this point in the history
* Handle parsing wildcard urls in launch profiles
- As part of this change expose TargetHostAddress on EndpointAnnotation which describes the original address that was bound to in both the kestrel endpoint list and the launch profile's application urls.
- Tell DCP about the target host
- Added a test

Fixes #5587
  • Loading branch information
davidfowl authored Sep 9, 2024
1 parent c3a4754 commit 15a23b5
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 12 deletions.
8 changes: 7 additions & 1 deletion src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public EndpointAnnotation(ProtocolType protocol, string? uriScheme = null, strin
/// <summary>
/// Desired port for the service
/// </summary>
public int? Port {
public int? Port
{
// For proxy-less Endpoints the client port and target port should be the same.
// Note that this is just a "sensible default"--the consumer of the EndpointAnnotation is free
// to change Port and TargetPort after the annotation is created, but if the final values are inconsistent,
Expand Down Expand Up @@ -104,6 +105,11 @@ public int? TargetPort
/// </summary>
public string UriScheme { get; set; }

/// <summary>
/// This is the address the resource is listening on. By default it is localhost.
/// </summary>
public string TargetHost { get; set; } = "localhost";

/// <summary>
/// Transport that is being used (e.g. http, http2, http3 etc).
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,7 @@ private void AddAllocatedEndpointInfo(IEnumerable<AppResource> resources)

sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(
sp.EndpointAnnotation,
sp.EndpointAnnotation.IsProxied ? svc.AllocatedAddress! : "localhost",
"localhost",
(int)svc.AllocatedPort!,
containerHostAddress: appResource.ModelResource.IsContainer() ? containerHost : null,
targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""");
Expand Down Expand Up @@ -977,6 +977,11 @@ private void PrepareServices()
var port = _options.Value.RandomizePorts && endpoint.IsProxied ? null : endpoint.Port;
svc.Spec.Port = port;
svc.Spec.Protocol = PortProtocol.FromProtocolType(endpoint.Protocol);
svc.Spec.Address = endpoint.TargetHost switch
{
"*" or "+" => "0.0.0.0",
_ => endpoint.TargetHost
};
svc.Spec.AddressAllocationMode = endpoint.IsProxied ? AddressAllocationModes.Localhost : AddressAllocationModes.Proxyless;

// So we can associate the service with the resource that produced it and the endpoint it represents.
Expand Down
24 changes: 15 additions & 9 deletions src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ private static IResourceBuilder<ProjectResource> WithProjectDefaults(this IResou
e.Port = endpoint.BindingAddress.Port;
}
e.UriScheme = endpoint.BindingAddress.Scheme;
e.TargetHost = endpoint.BindingAddress.Host;

adjustTransport(e, endpoint.Protocols);
// Keep track of the host separately since EndpointAnnotation doesn't have a host property
builder.Resource.KestrelEndpointAnnotationHosts[e] = endpoint.BindingAddress.Host;
Expand Down Expand Up @@ -405,27 +407,28 @@ private static IResourceBuilder<ProjectResource> WithProjectDefaults(this IResou
Dictionary<string, int> endpointCountByScheme = [];
foreach (var url in urlsFromApplicationUrl)
{
var uri = new Uri(url);
var bindingAddress = BindingAddress.Parse(url);

// Keep track of how many endpoints we have for each scheme
endpointCountByScheme.TryGetValue(uri.Scheme, out var count);
endpointCountByScheme[uri.Scheme] = count + 1;
endpointCountByScheme.TryGetValue(bindingAddress.Scheme, out var count);
endpointCountByScheme[bindingAddress.Scheme] = count + 1;

// If we have multiple for the same scheme, we differentiate them by appending a number.
// We only do this starting with the second endpoint, so that the first stays just http/https.
// This allows us to keep the same behavior as "dotnet run".
// Also, note that we only do this in Run mode, as in Publish mode those extra endpoints
// with generic names would not be easily usable.
var endpointName = uri.Scheme;
if (endpointCountByScheme[uri.Scheme] > 1)
var endpointName = bindingAddress.Scheme;
if (endpointCountByScheme[bindingAddress.Scheme] > 1)
{
endpointName += endpointCountByScheme[uri.Scheme];
endpointName += endpointCountByScheme[bindingAddress.Scheme];
}

builder.WithEndpoint(endpointName, e =>
{
e.Port = uri.Port;
e.UriScheme = uri.Scheme;
e.Port = bindingAddress.Port;
e.TargetHost = bindingAddress.Host;
e.UriScheme = bindingAddress.Scheme;
e.FromLaunchProfile = true;
adjustTransport(e);
},
Expand Down Expand Up @@ -640,7 +643,10 @@ private static void SetAspNetCoreUrls(this IResourceBuilder<ProjectResource> bui
processedHttpsPort = true;
}

aspnetCoreUrls.Append($"{e.Property(EndpointProperty.Scheme)}://localhost:{e.Property(EndpointProperty.TargetPort)}");
// If the endpoint is proxied, we will use localhost as the target host since DCP will be forwarding the traffic
var targetHost = e.EndpointAnnotation.IsProxied ? "localhost" : e.EndpointAnnotation.TargetHost;

aspnetCoreUrls.Append($"{e.Property(EndpointProperty.Scheme)}://{targetHost}:{e.Property(EndpointProperty.TargetPort)}");
first = false;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Aspire.Hosting.ApplicationModel.ConnectionStringReference.ConnectionName.get ->
Aspire.Hosting.ApplicationModel.ConnectionStringReference.ConnectionName.set -> void
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.get -> Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus?
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.HealthStatus.init -> void
Aspire.Hosting.ApplicationModel.EndpointAnnotation.TargetHost.get -> string!
Aspire.Hosting.ApplicationModel.EndpointAnnotation.TargetHost.set -> void
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Volumes.get -> System.Collections.Immutable.ImmutableArray<Aspire.Hosting.ApplicationModel.VolumeSnapshot!>
Aspire.Hosting.ApplicationModel.CustomResourceSnapshot.Volumes.init -> void
Aspire.Hosting.ApplicationModel.HealthCheckAnnotation
Expand Down
69 changes: 68 additions & 1 deletion tests/Aspire.Hosting.Tests/ProjectResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,52 @@ public async Task AddProjectWithArgs()
arg => Assert.Equal("http://localhost:1234", arg));
}

[Theory]
[InlineData(true, "localhost")]
[InlineData(false, "*")]
public async Task AddProjectWithWildcardUrlInLaunchSettings(bool isProxied, string expectedHost)
{
var appBuilder = CreateBuilder(operation: DistributedApplicationOperation.Run);

appBuilder.AddProject<TestProjectWithWildcardUrlInLaunchSettings>("projectName")
.WithEndpoint("http", e =>
{
Assert.Equal("*", e.TargetHost);
e.AllocatedEndpoint = new(e, "localhost", e.Port!.Value, targetPortExpression: "p0");
e.IsProxied = isProxied;
})
.WithEndpoint("https", e =>
{
Assert.Equal("*", e.TargetHost);
e.AllocatedEndpoint = new(e, "localhost", e.Port!.Value, targetPortExpression: "p1");
e.IsProxied = isProxied;
});

using var app = appBuilder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var projectResources = appModel.GetProjectResources();

var resource = Assert.Single(projectResources);

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);

var http = resource.GetEndpoint("http");
var https = resource.GetEndpoint("https");

if (isProxied)
{
// When the end point is proxied, the host should be localhost and the port should match the targetPortExpression
Assert.Equal($"http://{expectedHost}:p0;https://{expectedHost}:p1", config["ASPNETCORE_URLS"]);
}
else
{
Assert.Equal($"http://{expectedHost}:{http.TargetPort};https://{expectedHost}:{https.TargetPort}", config["ASPNETCORE_URLS"]);
}

Assert.Equal(https.Port.ToString(), config["ASPNETCORE_HTTPS_PORT"]);
}

internal static IDistributedApplicationBuilder CreateBuilder(string[]? args = null, DistributedApplicationOperation operation = DistributedApplicationOperation.Publish)
{
var resolvedArgs = new List<string>();
Expand Down Expand Up @@ -633,7 +679,7 @@ public TestProjectWithLaunchSettings()
{
Profiles = new()
{
["http"] = new ()
["http"] = new()
{
CommandName = "Project",
CommandLineArgs = "arg1 arg2",
Expand Down Expand Up @@ -668,4 +714,25 @@ public TestProjectWithManyAppUrlsInLaunchSettings()
};
}
}

private sealed class TestProjectWithWildcardUrlInLaunchSettings : BaseProjectWithProfileAndConfig
{
public TestProjectWithWildcardUrlInLaunchSettings()
{
Profiles = new()
{
["https"] = new()
{
CommandName = "Project",
CommandLineArgs = "arg1 arg2",
LaunchBrowser = true,
ApplicationUrl = "http://*:5031;https://*:5033",
EnvironmentVariables = new()
{
["ASPNETCORE_ENVIRONMENT"] = "Development"
}
}
};
}
}
}

0 comments on commit 15a23b5

Please sign in to comment.