diff --git a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs index 47fbc0a3932..4d8a53dab2a 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs @@ -19,8 +19,11 @@ internal class ExpressionResolver(string containerHostName, CancellationToken ca // If Container -> Container, we go directly to the container name and target port, bypassing the host (EndpointProperty.Host or EndpointProperty.IPV4Host, true) => target.Name, (EndpointProperty.Port, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false), - // If Container -> Exe, we need to go through the container host + (EndpointProperty.TargetPort, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false), + // If Container -> Exe or Exe -> Exe, we need to go through the container host for host, and use allocated port (EndpointProperty.Host or EndpointProperty.IPV4Host, false) => containerHostName, + (EndpointProperty.Port, false) => endpointReference.Port.ToString(CultureInfo.InvariantCulture), + (EndpointProperty.TargetPort, false) => endpointReference.Port.ToString(CultureInfo.InvariantCulture), (EndpointProperty.Url, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}", endpointReference.Scheme, await EvalEndpointAsync(endpointReference, EndpointProperty.Host).ConfigureAwait(false), @@ -132,8 +135,8 @@ async ValueTask ResolveInternalAsync(object? value) ConnectionStringReference cs => await ResolveConnectionStringReferenceAsync(cs).ConfigureAwait(false), IResourceWithConnectionString cs and not ConnectionStringParameterResource => await ResolveInternalAsync(cs.ConnectionStringExpression).ConfigureAwait(false), ReferenceExpression ex => await EvalExpressionAsync(ex).ConfigureAwait(false), - EndpointReference endpointReference when sourceIsContainer => new ResolvedValue(await EvalEndpointAsync(endpointReference, EndpointProperty.Url).ConfigureAwait(false), false), - EndpointReferenceExpression ep when sourceIsContainer => new ResolvedValue(await EvalEndpointAsync(ep.Endpoint, ep.Property).ConfigureAwait(false), false), + EndpointReference endpointReference => new ResolvedValue(await EvalEndpointAsync(endpointReference, EndpointProperty.Url).ConfigureAwait(false), false), + EndpointReferenceExpression ep => new ResolvedValue(await EvalEndpointAsync(ep.Endpoint, ep.Property).ConfigureAwait(false), false), IValueProvider vp => await EvalValueProvider(vp).ConfigureAwait(false), _ => throw new NotImplementedException() }; diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index fdddd5736f6..6ebdcb27a0b 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -184,6 +184,83 @@ public async Task ContainerToContainerEndpointShouldResolve() Assert.Equal("http://myContainer:8080", config["ConnectionStrings__myContainer"]); } + + [Theory] + [InlineData(false, false, "Target=12345;")] // executable -> executable + [InlineData(false, true, "Target=12345;")] // executable -> container + [InlineData(true, false, "Target=ContainerHostName:12345;")] // container -> executable + [InlineData(true, true, "Target=testresource:10000;")] // container -> container + public async Task ExecutableEndpointExpressionsShouldResolve(bool sourceIsContainer, bool targetIsContainer, string expectedResult) + { + var builder = DistributedApplication.CreateBuilder(); + + var target = builder.AddResource(new TestExecutableEndpointResource("testresource")) + .WithEndpoint("endpoint1", e => + { + e.UriScheme = "http"; + e.AllocatedEndpoint = new(e, "localhost", 12345, containerHostAddress: targetIsContainer ? "ContainerHostName" : null, targetPortExpression: "10000"); + }); + + if (targetIsContainer) + { + target = target.WithImage("someimage"); + } + + // Test endpoint port expression resolution for executable arguments + var endpointRef = new EndpointReference(target.Resource, "endpoint1"); + var portExpression = new EndpointReferenceExpression(endpointRef, EndpointProperty.Port); + + var result = await ExpressionResolver.ResolveAsync(sourceIsContainer, portExpression, "ContainerHostName", CancellationToken.None).DefaultTimeout(); + + var expected = expectedResult.Split('=')[1].TrimEnd(';'); + Assert.Equal(expected, result.Value); + } + + [Fact] + public async Task ExecutableWithEndpointArgumentsResolvesCorrectly() + { + var builder = DistributedApplication.CreateBuilder(); + + // Create a container resource with an endpoint + var container = builder.AddContainer("testcontainer", "nginx") + .WithHttpEndpoint(targetPort: 8080, port: 12345); + + // Create an executable that references the container's endpoint port + var executable = builder.AddExecutable("testexe", "pwsh.exe", ".") + .WithArgs("-port") + .WithArgs(x => x.Args.Add(container.GetEndpoint("http").Property(EndpointProperty.TargetPort))); + + // Test that the expression gets resolved for the executable (not container source) + var endpointRef = container.GetEndpoint("http"); + var portExpression = endpointRef.Property(EndpointProperty.TargetPort); + + var result = await ExpressionResolver.ResolveAsync(false, portExpression, "host.docker.internal", CancellationToken.None).DefaultTimeout(); + + // For executable accessing container endpoint, should get the host port (since target port would be for container-to-container) + Assert.Equal("12345", result.Value); + } + + [Fact] + public async Task ExecutableReferencingOwnEndpointTargetPortResolvesCorrectly() + { + var builder = DistributedApplication.CreateBuilder(); + + // Reproduce the exact scenario from the original issue + var app = builder.AddExecutable("exe", "pwsh.exe", ".") + .WithHttpEndpoint(port: 54321); + var endpoint = app.GetEndpoint("http"); + + app.WithArgs("-port") + .WithArgs(x => x.Args.Add(endpoint.Property(EndpointProperty.TargetPort))); + + // Test that the expression gets resolved when the executable references its own endpoint + var portExpression = endpoint.Property(EndpointProperty.TargetPort); + + var result = await ExpressionResolver.ResolveAsync(false, portExpression, "host.docker.internal", CancellationToken.None).DefaultTimeout(); + + // For executable accessing its own endpoint, should get the allocated port + Assert.Equal("54321", result.Value); + } } sealed class MyContainerResource : ContainerResource, IResourceWithConnectionString @@ -236,3 +313,10 @@ public TestExpressionResolverResource(string exprName) : base("testresource") public ReferenceExpression ConnectionStringExpression => Expressions[_exprName]; } + +sealed class TestExecutableEndpointResource : ContainerResource, IResourceWithEndpoints +{ + public TestExecutableEndpointResource(string name) : base(name) + { + } +}