Skip to content
29 changes: 0 additions & 29 deletions src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,6 @@ namespace Aspire.Hosting.ApplicationModel;

internal class ExpressionResolver(CancellationToken cancellationToken)
{

async Task<string?> ResolveInContainerContextAsync(EndpointReference endpointReference, EndpointProperty property, ValueProviderContext context)
{
// We need to use the root resource, e.g. AzureStorageResource instead of AzureBlobResource
// Otherwise, we get the wrong values for IsContainer and Name
var target = endpointReference.Resource.GetRootResource();

return (property, target.IsContainer()) switch
{
// If Container -> Container, we use <container name>.dev.internal as host, and target port as port
// This assumes both containers are on the same container network.
// Different networks will require addtional routing/tunneling that we do not support today.
(EndpointProperty.Host or EndpointProperty.IPV4Host, true) => $"{target.Name}.dev.internal",
(EndpointProperty.Port, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(context, cancellationToken).ConfigureAwait(false),

(EndpointProperty.Url, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}",
endpointReference.Scheme,
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
(EndpointProperty.HostAndPort, _) => string.Format(CultureInfo.InvariantCulture, "{0}:{1}",
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
_ => await endpointReference.Property(property).GetValueAsync(context, cancellationToken).ConfigureAwait(false)
};
}

async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr, ValueProviderContext context)
{
// This logic is similar to ReferenceExpression.GetValueAsync, except that we recurse on
Expand Down Expand Up @@ -95,14 +69,11 @@ 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 er.ContextNetworkID == KnownNetworkIdentifiers.DefaultAspireContainerNetwork || (er.ContextNetworkID == null && networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork) => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false),
EndpointReferenceExpression ep when ep.Endpoint.ContextNetworkID == KnownNetworkIdentifiers.DefaultAspireContainerNetwork || (ep.Endpoint.ContextNetworkID == null && 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
23 changes: 23 additions & 0 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,28 @@ private void AddAllocatedEndpointInfo(IEnumerable<RenderedModelResource> resourc
bindingMode,
targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""",
KnownNetworkIdentifiers.LocalhostNetwork);

if (appResource.DcpResource is Container ctr && ctr.Spec.Networks is not null)
{
foreach (var network in ctr.Spec.Networks)
{
var networkID = new NetworkIdentifier(network.Name!);
var address = network.Aliases!.Last(); // relying on an implementation detail here to ensure we get the `*.dev.internal` alias
Comment thread
karolz-ms marked this conversation as resolved.
Outdated
var port = sp.EndpointAnnotation.TargetPort!;

var tunnelAllocatedEndpoint = new AllocatedEndpoint(
sp.EndpointAnnotation,
address,
(int)port,
EndpointBindingMode.SingleAddress,
targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""",
networkID
);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(tunnelAllocatedEndpoint);
sp.EndpointAnnotation.AllAllocatedEndpoints.TryAdd(networkID, snapshot);
}
}
}
}

Expand Down Expand Up @@ -1040,6 +1062,7 @@ ts.Service is not null &&
}
}
}

}

private void PrepareContainerNetworks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public async Task AddContainerWithArgs()
e.AllocatedEndpoint = new(e, "localhost", 1234, targetPortExpression: "1234");

// For container-container lookup we need to add an AllocatedEndpoint on the container network side
var ccae = new AllocatedEndpoint(e, KnownHostNames.DefaultContainerTunnelHostName, 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var ccae = new AllocatedEndpoint(e, "c1.dev.internal", 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(ccae);
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
Expand All @@ -114,7 +114,7 @@ public async Task AddContainerWithArgs()
e.UriScheme = "http";
// We only care about the container-side endpoint for this test
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
var ae = new AllocatedEndpoint(e, "localhost", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var ae = new AllocatedEndpoint(e, "container.dev.internal", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
snapshot.SetValue(ae);
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
})
Expand Down
102 changes: 101 additions & 1 deletion tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ public async Task EndpointPortsConainerProxiedNoPortTargetPortSet()
}

[Fact]
public async Task EndpointPortsConainerProxiedPortAndTargetPortSet()
public async Task EndpointPortsContainerProxiedPortAndTargetPortSet()
{
var builder = DistributedApplication.CreateBuilder();

Expand Down Expand Up @@ -2175,6 +2175,106 @@ public async Task ProjectExecutable_NoSupportsDebuggingAnnotation_RunsInProcessM
Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType);
}

[Theory]
[InlineData(true, null, "aspire.dev.internal")]
[InlineData(false, null, "host.docker.internal")]
[InlineData(true, "super.star", "aspire.dev.internal")]
[InlineData(false, "mega.mushroom", "mega.mushroom")]
public async Task EndpointsAllocatedCorrectly(bool useTunnel, string? containerHostName, string expectedContainerHost)
{
var builder = DistributedApplication.CreateBuilder();
var executable = builder.AddExecutable("anExecutable", "command", "")
.WithEndpoint(name: "proxied", targetPort: 1234, port: 5678, isProxied: true)
.WithEndpoint(name: "notProxied", port: 8765, isProxied: false);

var container = builder.AddContainer("aContainer", "image")
.WithEndpoint(name: "proxied", port: 15678, targetPort: 11234, isProxied: true)
.WithEndpoint(name: "notProxied", port: 18765, isProxied: false);

var containerWithAlias = builder.AddContainer("containerWithAlias", "image")
.WithEndpoint(name: "proxied", port: 25678, targetPort: 21234, isProxied: true)
.WithEndpoint(name: "notProxied", port: 28765, isProxied: false)
.WithContainerNetworkAlias("custom.alias");

var kubernetesService = new TestKubernetesService();
using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var configDict = new Dictionary<string, string?>
{
["AppHost:ContainerHostname"] = containerHostName
};
var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build();

var dcpOptions = new DcpOptions
{
EnableAspireContainerTunnel = useTunnel,
};

var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration, dcpOptions: dcpOptions);

await appExecutor.RunApplicationAsync();

await AssertEndpoint(executable.Resource, "proxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 5678);
await AssertEndpoint(executable.Resource, "notProxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 8765);

if (useTunnel)
{
await AssertTunneledPort(executable.Resource, "proxied");
await AssertTunneledPort(executable.Resource, "notProxied");

async ValueTask AssertTunneledPort(IResourceWithEndpoints resource, string endpointName)
{
var svcs = kubernetesService.CreatedResources
.OfType<Service>()
.Where(x => x.AppModelResourceName == resource.Name
&& x.EndpointName == endpointName
&& x.Metadata.Annotations.ContainsKey(CustomResource.ContainerTunnelInstanceName))
.ToList();

var svc = svcs.Single();

int port = svc.AllocatedPort!.Value;
await AssertEndpoint(executable.Resource, endpointName, KnownNetworkIdentifiers.DefaultAspireContainerNetwork, expectedContainerHost, port);
}
}
else
{
await AssertEndpoint(executable.Resource, "proxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, expectedContainerHost, 5678);
await AssertEndpoint(executable.Resource, "notProxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, expectedContainerHost, 8765);
}

await AssertEndpoint(container.Resource, "proxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 15678);
await AssertEndpoint(container.Resource, "notProxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 18765);

await AssertEndpoint(container.Resource, "proxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, $"{container.Resource.Name}.dev.internal", 11234);
await AssertEndpoint(container.Resource, "notProxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, $"{container.Resource.Name}.dev.internal", 18765);

await AssertEndpoint(containerWithAlias.Resource, "proxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 25678);
await AssertEndpoint(containerWithAlias.Resource, "notProxied", KnownNetworkIdentifiers.LocalhostNetwork, KnownHostNames.Localhost, 28765);

//TODO: Not sure if this is the desired behaviour
Comment thread
karolz-ms marked this conversation as resolved.
Outdated
await AssertEndpoint(containerWithAlias.Resource, "proxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, "custom.alias", 21234);
await AssertEndpoint(containerWithAlias.Resource, "notProxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, "custom.alias", 28765);

async ValueTask AssertEndpoint(IResourceWithEndpoints resource, string name, NetworkIdentifier network, string address, int port)
{
var endpoint = resource.GetEndpoint(name).EndpointAnnotation;
var allocatedEndpoints = endpoint.AllAllocatedEndpoints;

Assert.Contains(allocatedEndpoints, a => a.NetworkID == network);

var allocatedEndpoint = await endpoint.AllAllocatedEndpoints.Single(x => x.NetworkID == network).Snapshot.GetValueAsync().DefaultTimeout();

Assert.Equal(endpoint, allocatedEndpoint.Endpoint);
Assert.Equal(address, allocatedEndpoint.Address);
Assert.Equal(EndpointBindingMode.SingleAddress, allocatedEndpoint.BindingMode);
Assert.Equal(port, allocatedEndpoint.Port);
Assert.Equal(endpoint.UriScheme, allocatedEndpoint.UriScheme);
Assert.Equal($"{address}:{port}", allocatedEndpoint.EndPointString);
}
}

private static void HasKnownCommandAnnotations(IResource resource)
{
var commandAnnotations = resource.Annotations.OfType<ResourceCommandAnnotation>().ToList();
Expand Down
84 changes: 84 additions & 0 deletions tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,90 @@ public void TargetPort_ReturnsNullWhenNotDefined()
Assert.Null(targetPort);
}

[Theory]
[InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Host, "blah://localhost:1234")]
[InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Container, "blah://localhost:1234")]
[InlineData(EndpointProperty.Url, ResourceKind.Container, ResourceKind.Host, "blah://host.docker.internal:1234")]
[InlineData(EndpointProperty.Url, ResourceKind.Container, ResourceKind.Container, "blah://destination.dev.internal:4567")]
[InlineData(EndpointProperty.Host, ResourceKind.Host, ResourceKind.Host, "localhost")]
[InlineData(EndpointProperty.Host, ResourceKind.Host, ResourceKind.Container, "localhost")]
[InlineData(EndpointProperty.Host, ResourceKind.Container, ResourceKind.Host, "host.docker.internal")]
[InlineData(EndpointProperty.Host, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal")]
[InlineData(EndpointProperty.IPV4Host, ResourceKind.Host, ResourceKind.Host, "127.0.0.1")]
[InlineData(EndpointProperty.IPV4Host, ResourceKind.Host, ResourceKind.Container, "127.0.0.1")]
[InlineData(EndpointProperty.IPV4Host, ResourceKind.Container, ResourceKind.Host, "host.docker.internal")]
[InlineData(EndpointProperty.IPV4Host, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal")]
[InlineData(EndpointProperty.Port, ResourceKind.Host, ResourceKind.Host, "1234")]
[InlineData(EndpointProperty.Port, ResourceKind.Host, ResourceKind.Container, "1234")]
[InlineData(EndpointProperty.Port, ResourceKind.Container, ResourceKind.Host, "1234")]
[InlineData(EndpointProperty.Port, ResourceKind.Container, ResourceKind.Container, "4567")]
[InlineData(EndpointProperty.Scheme, ResourceKind.Host, ResourceKind.Host, "blah")]
[InlineData(EndpointProperty.Scheme, ResourceKind.Host, ResourceKind.Container, "blah")]
[InlineData(EndpointProperty.Scheme, ResourceKind.Container, ResourceKind.Host, "blah")]
[InlineData(EndpointProperty.Scheme, ResourceKind.Container, ResourceKind.Container, "blah")]
[InlineData(EndpointProperty.HostAndPort, ResourceKind.Host, ResourceKind.Host, "localhost:1234")]
[InlineData(EndpointProperty.HostAndPort, ResourceKind.Host, ResourceKind.Container, "localhost:1234")]
[InlineData(EndpointProperty.HostAndPort, ResourceKind.Container, ResourceKind.Host, "host.docker.internal:1234")]
[InlineData(EndpointProperty.HostAndPort, ResourceKind.Container, ResourceKind.Container, "destination.dev.internal:4567")]
public async Task PropertyResolutionTest(EndpointProperty property, ResourceKind sourceKind, ResourceKind destinationKind, object expectedResult)
{
int port = 1234;
int targetPort = 4567;

var source = CreateResource("caller", sourceKind);
var destination = CreateResource("destination", destinationKind);

var network = source.GetDefaultResourceNetwork();

// This logic is tightly coupled to how `DcpExecutor` allocates endpoints
var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "blah", name: "http");
annotation.AllocatedEndpoint = new(annotation, "localhost", port);
destination.Annotations.Add(annotation);

(string containerHost, int containerPort) = destination.IsContainer()
? ("destination.dev.internal", targetPort)
: ("host.docker.internal", port);

var containerEndpoint = new AllocatedEndpoint(annotation, containerHost, containerPort, EndpointBindingMode.SingleAddress, targetPortExpression: targetPort.ToString(), KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(containerEndpoint);
annotation.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);

var expression = destination.GetEndpoint(annotation.Name).Property(property);

var resultFromCaller = await expression.GetValueAsync(new ValueProviderContext
{
Caller = source
});
Assert.Equal(expectedResult, resultFromCaller);

var resultFromNetwork = await expression.GetValueAsync(new ValueProviderContext
{
Network = network
});
Assert.Equal(expectedResult, resultFromNetwork);

static IResourceWithEndpoints CreateResource(string name, ResourceKind kind)
{
if (kind == ResourceKind.Container)
{
var resource = new TestResource(name);
resource.Annotations.Add(new ContainerImageAnnotation { Image = "test-image" });
return resource;
}
else
{
return new TestResource(name);
}
}
}

public enum ResourceKind
{
Host,
Container
}

private sealed class TestResource(string name) : Resource(name), IResourceWithEndpoints
{
}
Expand Down
Loading
Loading