Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle parsing wildcard urls in launch profiles #5588

Merged
merged 7 commits into from
Sep 9, 2024
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
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"
}
}
};
}
}
}