Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions playground/yarp/Yarp.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Aspire.Hosting.Yarp.Transforms;

var builder = DistributedApplication.CreateBuilder(args);
builder.Configuration["DcpPublisher:EnableAspireContainerTunnel"] = "true";

var backendService = builder.AddProject<Projects.Yarp_Backend>("backend");

Expand Down
16 changes: 3 additions & 13 deletions src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,12 @@ public class ConnectionStringReference(IResourceWithConnectionString resource, b

ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken)
{
return this.GetNetworkValueAsync(null, cancellationToken);
return Resource.GetValueAsync(cancellationToken);
}

ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken)
async ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken)
{
return context.Network switch
{
NetworkIdentifier networkContext => GetNetworkValueAsync(networkContext, cancellationToken),
_ => GetNetworkValueAsync(null, cancellationToken)
};
}

private async ValueTask<string?> GetNetworkValueAsync(NetworkIdentifier? networkContext, CancellationToken cancellationToken)
{
ValueProviderContext vpc = new() { Network = networkContext };
var value = await Resource.GetValueAsync(vpc, cancellationToken).ConfigureAwait(false);
var value = await Resource.GetValueAsync(context, cancellationToken).ConfigureAwait(false);

if (string.IsNullOrEmpty(value) && !Optional)
{
Expand Down
40 changes: 16 additions & 24 deletions src/Aspire.Hosting/ApplicationModel/EndpointReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi
// A reference to the endpoint annotation if it exists.
private EndpointAnnotation? _endpointAnnotation;
private bool? _isAllocated;
private readonly NetworkIdentifier _contextNetworkID;
private readonly NetworkIdentifier? _contextNetworkID;

/// <summary>
/// Gets the endpoint annotation associated with the endpoint reference.
Expand Down Expand Up @@ -71,7 +71,7 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi
/// The reference will be resolved in the context of this network, which may be different
/// from the network associated with the default network of the referenced Endpoint.
/// </summary>
public NetworkIdentifier ContextNetworkID => _contextNetworkID;
public NetworkIdentifier? ContextNetworkID => _contextNetworkID;

/// <summary>
/// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified.
Expand Down Expand Up @@ -177,20 +177,20 @@ public EndpointReferenceExpression Property(EndpointProperty property)
/// <param name="endpoint">The endpoint annotation.</param>
/// <param name="contextNetworkID">The ID of the network that serves as the context for the EndpointReference.</param>
/// <remarks>
/// Most Aspire resources are accessed in the context of the "localhost" network (host proceses calling other host processes,
/// or host processes calling container via mapped ports). This is why EndpointReference assumes this
/// context unless specified otherwise. However, for container-to-container, or container-to-host communication,
/// you must specify a container network context for the EndpointReference to be resolved correctly.
/// Most Aspire resources are accessed in the context of the "localhost" network (host processes calling other host processes,
/// or host processes calling container via mapped ports). If a <see cref="NetworkIdentifier"/> is specified, the <see cref="EndpointReference"/>
/// will always resolve in the context of that network. If the <see cref="NetworkIdentifier"/> is null, the reference will attempt to resolve itself
/// based on the context of the requesting resource.
/// </remarks>
public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID = null)
public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID)
{
ArgumentNullException.ThrowIfNull(owner);
ArgumentNullException.ThrowIfNull(endpoint);

Resource = owner;
EndpointName = endpoint.Name;
_endpointAnnotation = endpoint;
_contextNetworkID = contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork;
_contextNetworkID = contextNetworkID;
}

/// <summary>
Expand Down Expand Up @@ -221,7 +221,7 @@ public EndpointReference(IResourceWithEndpoints owner, string endpointName, Netw

Resource = owner;
EndpointName = endpointName;
_contextNetworkID = contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork;
_contextNetworkID = contextNetworkID;
}

/// <summary>
Expand Down Expand Up @@ -265,7 +265,7 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En
/// <exception cref="InvalidOperationException">Throws when the selected <see cref="EndpointProperty"/> enumeration is not known.</exception>
public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default)
{
return GetNetworkValueAsync(null, cancellationToken);
return GetValueAsync(new(), cancellationToken);
}

/// <summary>
Expand All @@ -275,33 +275,25 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En
/// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="string"/> containing the selected <see cref="EndpointProperty"/> value.</returns>
/// <exception cref="InvalidOperationException">Throws when the selected <see cref="EndpointProperty"/> enumeration is not known.</exception>
public ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default)
public async ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default)
{
return context.Network switch
{
NetworkIdentifier networkID => GetNetworkValueAsync(networkID, cancellationToken),
_ => GetNetworkValueAsync(null, cancellationToken)
};
}
// If the EndpointReference was for a specific network context, then use that. Otherwise, use the network context from the ValueProviderContext.
// This allows the EndpointReference to be resolved in the context of the caller's network if it was not explicitly set.
var networkContext = Endpoint.ContextNetworkID ?? context.GetNetworkIdentifier();


private async ValueTask<string?> GetNetworkValueAsync(NetworkIdentifier? context, CancellationToken cancellationToken = default)
{
return Property switch
{
EndpointProperty.Scheme => new(Endpoint.Scheme),
EndpointProperty.IPV4Host when context is null || context == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1",
EndpointProperty.IPV4Host when networkContext == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1",
EndpointProperty.TargetPort when Endpoint.TargetPort is int port => new(port.ToString(CultureInfo.InvariantCulture)),
_ => await ResolveValueWithAllocatedAddress().ConfigureAwait(false)
};

async ValueTask<string?> ResolveValueWithAllocatedAddress()
{
var effectiveContext = context ?? Endpoint.ContextNetworkID;

// We are going to take the first snapshot that matches the context network ID. In general there might be multiple endpoints for a single service,
// and in future we might need some sort of policy to choose between them, but for now we just take the first one.
var nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == effectiveContext).FirstOrDefault();
var nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == networkContext).FirstOrDefault();
if (nes is null)
{
return null;
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,14 @@ async Task<ResolvedValue> ResolveConnectionStringReferenceAsync(ConnectionString
/// </summary>
async ValueTask<ResolvedValue> ResolveInternalAsync(object? value, ValueProviderContext context)
{
var networkContext = context.GetNetworkIdentifier();
return value switch
{
ConnectionStringReference cs => await ResolveConnectionStringReferenceAsync(cs, context).ConfigureAwait(false),
IResourceWithConnectionString cs and not ConnectionStringParameterResource => await ResolveInternalAsync(cs.ConnectionStringExpression, context).ConfigureAwait(false),
ReferenceExpression ex => await EvalExpressionAsync(ex, context).ConfigureAwait(false),
EndpointReference er when context.Network == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false),
EndpointReferenceExpression ep when context.Network == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(ep.Endpoint, ep.Property, context).ConfigureAwait(false), false),
EndpointReference er when networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false),
EndpointReferenceExpression ep when networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(ep.Endpoint, ep.Property, context).ConfigureAwait(false), false),
IValueProvider vp => await EvalValueProvider(vp, context).ConfigureAwait(false),
_ => throw new NotImplementedException()
};
Expand Down
84 changes: 63 additions & 21 deletions src/Aspire.Hosting/ApplicationModel/HostUrl.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.Dcp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
Expand All @@ -13,26 +17,19 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider
string IManifestExpressionProvider.ValueExpression => Url;

// Returns the url
ValueTask<string?> IValueProvider.GetValueAsync(System.Threading.CancellationToken _) => GetNetworkValueAsync(null);
ValueTask<string?> IValueProvider.GetValueAsync(System.Threading.CancellationToken cancellationToken) => ((IValueProvider)this).GetValueAsync(new(), cancellationToken);

// Returns the url
ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken _)
async ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken)
{
return context.Network switch
{
NetworkIdentifier networkContext => GetNetworkValueAsync(networkContext),
_ => GetNetworkValueAsync(null)
};
}
var networkContext = context.GetNetworkIdentifier();

private ValueTask<string?> GetNetworkValueAsync(NetworkIdentifier? context)
{
// HostUrl is a bit of a hack that is not modeled as an expression
// So in this one case, we need to fix up the container host name 'manually'
// Internally, this is only used for OTEL_EXPORTER_OTLP_ENDPOINT, but HostUrl
// is public, so we don't control how it is used

if (context is null || context == KnownNetworkIdentifiers.LocalhostNetwork)
if (networkContext == KnownNetworkIdentifiers.LocalhostNetwork)
{
return new(Url);
}
Expand All @@ -44,25 +41,70 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider
var uri = new UriBuilder(Url);
if (uri.Host is "localhost" or "127.0.0.1" or "[::1]")
{
var hasEndingSlash = Url.EndsWith('/');
uri.Host = KnownHostNames.DefaultContainerTunnelHostName;
retval = uri.ToString();

// Remove trailing slash if we didn't have one before (UriBuilder always adds one)
if (!hasEndingSlash && retval.EndsWith('/'))
if (context.ExecutionContext?.IsRunMode == true)
{
retval = retval[..^1];
var options = context.ExecutionContext.ServiceProvider.GetRequiredService<IOptions<DcpOptions>>();

var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService<IDcpDependencyCheckService>();
var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false);

uri.Host = options.Value.EnableAspireContainerTunnel? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge;

if (options.Value.EnableAspireContainerTunnel)
{
// We need to consider that both the host and port may need to be remapped
var model = context.ExecutionContext.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
var targetEndpoint = model.Resources.Where(r => !r.IsContainer())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to scan the entire model??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All we know in HostUrl is an address and port, but both the address and port may need to change if running in a container. So I'm finding out if it's the URL to an existing endpoint and then mapping that to the appropriate container network endpoint if one exists. Ideally we'd just have a way to get the endpoints from the dashboard directly here and deprecate HostUrl entirely, but that's a larger change.

.OfType<IResourceWithEndpoints>()
.Select(r =>
{
if (r.GetEndpoints(KnownNetworkIdentifiers.LocalhostNetwork).FirstOrDefault(ep => ep.Port == uri.Port) is EndpointReference ep)
{
return r.GetEndpoint(ep.EndpointName, networkContext);
}

return null;
})
.Where(ep => ep is not null)
.FirstOrDefault();

if (targetEndpoint is { })
{
uri.Port = targetEndpoint.Port;
}
}

retval = uri.ToString();
}
}

var hasEndingSlash = Url.EndsWith('/');

// Remove trailing slash if we didn't have one before (UriBuilder always adds one)
if (!hasEndingSlash && retval.EndsWith('/'))
{
retval = retval[..^1];
}
}
catch (UriFormatException)
{
var replacementHost = KnownHostNames.DockerDesktopHostBridge;
if (context.ExecutionContext?.IsRunMode == true)
{
var options = context.ExecutionContext.ServiceProvider.GetRequiredService<IOptions<DcpOptions>>();

var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService<IDcpDependencyCheckService>();
var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false);

replacementHost = options.Value.EnableAspireContainerTunnel ? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge;
}

// HostUrl was meant to only be used with valid URLs. However, this was not
// previously enforced. So we need to handle the case where it's not a valid URL,
// by falling back to a simple string replacement.
retval = retval.Replace(KnownHostNames.Localhost, KnownHostNames.DefaultContainerTunnelHostName, StringComparison.OrdinalIgnoreCase)
.Replace("127.0.0.1", KnownHostNames.DefaultContainerTunnelHostName)
.Replace("[::1]", KnownHostNames.DefaultContainerTunnelHostName);
retval = retval.Replace(KnownHostNames.Localhost, replacementHost, StringComparison.OrdinalIgnoreCase)
.Replace("127.0.0.1", replacementHost)
.Replace("[::1]", replacementHost);
}

return new(retval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ public interface IResourceWithConnectionString : IResource, IManifestExpressionP

ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken) => GetConnectionStringAsync(cancellationToken);

ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) => context.Network switch
{
NetworkIdentifier networkContext => ConnectionStringExpression.GetValueAsync(new ValueProviderContext { Network = networkContext }, cancellationToken),
_ => GetConnectionStringAsync(cancellationToken),
};
ValueTask<string?> IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) =>
ConnectionStringExpression.GetValueAsync(context, cancellationToken);

/// <summary>
/// Describes the connection string format string used for this resource.
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/ApplicationModel/IValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace Aspire.Hosting.ApplicationModel;
public class ValueProviderContext
{
/// <summary>
/// Additional services that can be used during value resolution.
/// The execution context for the distributed application.
/// </summary>
public IServiceProvider? Services { get; init; }
public DistributedApplicationExecutionContext? ExecutionContext { get; init; }

/// <summary>
/// The resource that is requesting the value.
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private ReferenceExpression(string format, IValueProvider[] valueProviders, stri
/// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken)
{
return this.GetValueAsync(new ValueProviderContext(), cancellationToken);
return this.GetValueAsync(new(), cancellationToken);
}

internal static ReferenceExpression Create(string format, IValueProvider[] valueProviders, string[] manifestExpressions, string?[] stringFormats)
Expand Down
Loading
Loading