diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index a4009d690bc..8ee659e3040 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -244,6 +244,7 @@ public AllocatedEndpoint? AllocatedEndpoint /// /// AllocatedEndpoint snapshot /// The ID of the network that is associated with the AllocatedEndpoint snapshot. +[DebuggerDisplay("NetworkID = {NetworkID}, Endpoint = {Snapshot}")] public record class NetworkEndpointSnapshot(ValueSnapshot Snapshot, NetworkIdentifier NetworkID); /// @@ -251,6 +252,7 @@ public record class NetworkEndpointSnapshot(ValueSnapshot Sna /// public class NetworkEndpointSnapshotList : IEnumerable { + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] private readonly ConcurrentBag _snapshots = new(); /// diff --git a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs index b430a9c7b47..c483a8a2360 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs @@ -8,32 +8,6 @@ namespace Aspire.Hosting.ApplicationModel; internal class ExpressionResolver(CancellationToken cancellationToken) { - - async Task 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 .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 EvalExpressionAsync(ReferenceExpression expr, ValueProviderContext context) { // This logic is similar to ReferenceExpression.GetValueAsync, except that we recurse on @@ -95,14 +69,11 @@ async Task ResolveConnectionStringReferenceAsync(ConnectionString /// async ValueTask 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() }; diff --git a/src/Aspire.Hosting/ApplicationModel/ValueSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/ValueSnapshot.cs index 47081560cfe..003137933e9 100644 --- a/src/Aspire.Hosting/ApplicationModel/ValueSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/ValueSnapshot.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; + namespace Aspire.Hosting.ApplicationModel; /// @@ -12,6 +14,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Thread-safe for concurrent SetValue / SetException / GetValueAsync calls. /// +[DebuggerDisplay("{Value= {DebuggerValue()}")] public sealed class ValueSnapshot where T : notnull { private readonly TaskCompletionSource _firstValueTcs = @@ -73,4 +76,5 @@ public void SetException(Exception exception) } } } + private T? DebuggerValue() => IsValueSet ? _firstValueTcs.Task.Result : default; } diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 1d6b253359c..df379b2f253 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -977,6 +977,29 @@ private void AddAllocatedEndpointInfo(IEnumerable resourc bindingMode, targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", KnownNetworkIdentifiers.LocalhostNetwork); + + if (appResource.DcpResource is Container ctr && ctr.Spec.Networks is not null) + { + // Once container networks are fully supported, this should allocate endpoints on those networks + var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); + + if (containerNetwork is not null) + { + var port = sp.EndpointAnnotation.TargetPort!; + + var allocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + $"{sp.ModelResource.Name}.dev.internal", + (int)port, + EndpointBindingMode.SingleAddress, + targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.DefaultAspireContainerNetwork + ); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(allocatedEndpoint); + sp.EndpointAnnotation.AllAllocatedEndpoints.TryAdd(allocatedEndpoint.NetworkID, snapshot); + } + } } } @@ -1040,6 +1063,7 @@ ts.Service is not null && } } } + } private void PrepareContainerNetworks() diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index 53fe1bb9528..a7f55cc7a7a 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -368,7 +368,13 @@ public async Task WithPostgresMcpOnAzureDatabaseRunAsContainerAddsMcpResource() .WithPasswordAuthentication(userName: user, password: pass) .RunAsContainer(c => { - c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432)); + c.WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); }); var db = postgres.AddDatabase("db") diff --git a/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs b/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs index 335bbcac08b..06495623fc2 100644 --- a/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs +++ b/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs @@ -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(); snapshot.SetValue(ccae); e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); @@ -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(); - 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); }) diff --git a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs index 474e5458d2c..433b15b1a46 100644 --- a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs +++ b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs @@ -96,7 +96,13 @@ public async Task MilvusClientAppWithReferenceContainsConnectionStrings() var pass = appBuilder.AddParameter("apikey", "pass"); var milvus = appBuilder.AddMilvus("my-milvus", pass) - .WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc)); + .WithEndpoint("grpc", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) .WithReference(milvus); diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs index 42272057861..e2eb3b0de6d 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs @@ -75,7 +75,13 @@ public async Task WithPostgresMcpOnDatabaseSetsDatabaseUriEnvironmentVariable() var pass = appBuilder.AddParameter("pass", "p@ssw0rd1"); appBuilder.AddPostgres("postgres", password: pass) - .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432)) + .WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }) .AddDatabase("db") .WithPostgresMcp(); diff --git a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs index c9d2a819290..4d50b6207c1 100644 --- a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs +++ b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs @@ -169,8 +169,20 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings() var pass = appBuilder.AddParameter("pass", "pass"); var qdrant = appBuilder.AddQdrant("my-qdrant", pass) - .WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334)) - .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333)); + .WithEndpoint("grpc", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }) + .WithEndpoint("http", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) .WithReference(qdrant); diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index 362b9079ea7..cace9589d26 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -301,8 +301,27 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables() using var app = builder.Build(); // Add fake allocated endpoints. - redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001)); - redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002)); + redis1.WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); + redis2.WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); + redis3.WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5003); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }); var redisInsight = Assert.Single(builder.Resources.OfType()); var envs = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(redisInsight); @@ -367,7 +386,6 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables() Assert.Equal("RI_REDIS_ALIAS3", item.Key); Assert.Equal(redis3.Resource.Name, item.Value); }); - } [Fact] @@ -713,7 +731,13 @@ public async Task RedisInsightEnvironmentCallbackIsIdempotent() using var appBuilder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper); var redis = appBuilder.AddRedis("redis") - .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379)) + .WithEndpoint("tcp", e => + { + e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + }) .WithRedisInsight(); using var app = appBuilder.Build(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 8840c964dd1..8ccc1004794 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -1000,7 +1000,7 @@ public async Task EndpointPortsConainerProxiedNoPortTargetPortSet() } [Fact] - public async Task EndpointPortsConainerProxiedPortAndTargetPortSet() + public async Task EndpointPortsContainerProxiedPortAndTargetPortSet() { var builder = DistributedApplication.CreateBuilder(); @@ -2175,6 +2175,105 @@ 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(); + + var configDict = new Dictionary + { + ["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() + .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); + + await AssertEndpoint(containerWithAlias.Resource, "proxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, $"{containerWithAlias.Resource.Name}.dev.internal", 21234); + await AssertEndpoint(containerWithAlias.Resource, "notProxied", KnownNetworkIdentifiers.DefaultAspireContainerNetwork, $"{containerWithAlias.Resource.Name}.dev.internal", 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().ToList(); diff --git a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs index f7fd6771288..11e3e3d7de6 100644 --- a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs @@ -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(); + 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 { } diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index 88bbcdd48fd..429757d98a8 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -84,6 +84,10 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN { var builder = DistributedApplication.CreateBuilder(); + var containerHost = targetIsContainer + ? "testresource.dev.internal" + : KnownHostNames.DefaultContainerTunnelHostName; + var target = builder.AddResource(new TestExpressionResolverResource(exprName)) .WithEndpoint("endpoint1", e => { @@ -92,31 +96,31 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN if (sourceIsContainer) { // Note: on the container network side the port and target port are always the same for AllocatedEndpoint. - var ae = new AllocatedEndpoint(e, KnownHostNames.DefaultContainerTunnelHostName, 22345, EndpointBindingMode.SingleAddress, targetPortExpression: "22345", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + var ae = new AllocatedEndpoint(e, containerHost, 22345, EndpointBindingMode.SingleAddress, targetPortExpression: "22345", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); var snapshot = new ValueSnapshot(); snapshot.SetValue(ae); e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); } }) .WithEndpoint("endpoint2", e => - { - e.UriScheme = "https"; - e.AllocatedEndpoint = new(e, "localhost", 12346, targetPortExpression: "10001"); - if (sourceIsContainer) - { - var ae = new AllocatedEndpoint(e, KnownHostNames.DefaultContainerTunnelHostName, 22346, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); - } - }) + { + e.UriScheme = "https"; + e.AllocatedEndpoint = new(e, "localhost", 12346, targetPortExpression: "10001"); + if (sourceIsContainer) + { + var ae = new AllocatedEndpoint(e, containerHost, 22346, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(ae); + e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + } + }) .WithEndpoint("endpoint3", e => { e.UriScheme = "https"; e.AllocatedEndpoint = new(e, "host with space", 12347); if (sourceIsContainer) { - var ae = new AllocatedEndpoint(e, KnownHostNames.DefaultContainerTunnelHostName, 22347, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + var ae = new AllocatedEndpoint(e, containerHost, 22347, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); var snapshot = new ValueSnapshot(); snapshot.SetValue(ae); e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); @@ -217,10 +221,18 @@ public async Task ContainerToContainerEndpointShouldResolve() { var builder = DistributedApplication.CreateBuilder(); + var endpoint = new EndpointAnnotation(System.Net.Sockets.ProtocolType.Tcp, KnownNetworkIdentifiers.DefaultAspireContainerNetwork) + { + Name = "http", + UriScheme = "http", + Port = 8001, + TargetPort = 8080, + }; + endpoint.AllocatedEndpoint = new(endpoint, "myContainer.dev.internal", (int)endpoint.TargetPort, EndpointBindingMode.SingleAddress, "{{ targetPort }}"); + var connectionStringResource = builder.AddResource(new MyContainerResource("myContainer")) .WithImage("redis") - .WithHttpEndpoint(port: 8001, targetPort: 8080) - .WithEndpoint("http", ep => ep.AllocatedEndpoint = new(ep, "localhost", 8001, EndpointBindingMode.SingleAddress, "{{ targetPort }}", KnownNetworkIdentifiers.LocalhostNetwork)); + .WithAnnotation(endpoint); var dep = builder.AddContainer("container", "redis") .WithReference(connectionStringResource) diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index dd40af66a63..c6b9b0a1070 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -227,7 +227,12 @@ public async Task EnvironmentVariableExpressions() .WithHttpEndpoint(name: "primary", targetPort: 10005) .WithEndpoint("primary", ep => { - ep.AllocatedEndpoint = new AllocatedEndpoint(ep, "localhost", 90); + ep.AllocatedEndpoint = new AllocatedEndpoint(ep, "localhost", 17454); + + var ae = new AllocatedEndpoint(ep, "container1.dev.internal", 10005, EndpointBindingMode.SingleAddress, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + var snapshot = new ValueSnapshot(); + snapshot.SetValue(ae); + ep.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); }); var endpoint = container.GetEndpoint("primary");