Skip to content

Commit 87857ff

Browse files
CopilotdavidfowlCopilot
authored
Fix endpoint resolution for individual Host/Port properties in WithEnvironment (#10892)
* Initial plan * Fix endpoint resolution for individual Host/Port properties in WithEnvironment Co-authored-by: davidfowl <[email protected]> * Remove unused preprocessing infrastructure from ExpressionResolver Co-authored-by: davidfowl <[email protected]> * Update test data for ExpressionResolver and fix expected PORT value in WithEnvironmentTests * Update src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs Co-authored-by: Copilot <[email protected]> * Fix Kafka host port in run mode --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: David Fowler <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent f3fc039 commit 87857ff

File tree

4 files changed

+14
-65
lines changed

4 files changed

+14
-65
lines changed

src/Aspire.Hosting.Kafka/KafkaBuilderExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Globalization;
45
using Aspire.Hosting.ApplicationModel;
56
using Confluent.Kafka;
67
using HealthChecks.Kafka;
@@ -214,7 +215,7 @@ private static void ConfigureKafkaContainer(EnvironmentCallbackContext context,
214215
var advertisedListeners = context.ExecutionContext.IsRunMode
215216
// In run mode, PLAINTEXT_INTERNAL assumes kafka is being accessed over a default Aspire container network and hardcodes the resource address
216217
// This will need to be refactored once updated service discovery APIs are available
217-
? ReferenceExpression.Create($"PLAINTEXT://localhost:29092,PLAINTEXT_HOST://localhost:{primaryEndpoint.Property(EndpointProperty.Port)},PLAINTEXT_INTERNAL://{resource.Name}:{internalEndpoint.Property(EndpointProperty.TargetPort)}")
218+
? ReferenceExpression.Create($"PLAINTEXT://localhost:29092,PLAINTEXT_HOST://localhost:{primaryEndpoint.Port.ToString(CultureInfo.InvariantCulture)},PLAINTEXT_INTERNAL://{resource.Name}:{internalEndpoint.Property(EndpointProperty.TargetPort)}")
218219
: ReferenceExpression.Create($"PLAINTEXT://{primaryEndpoint.Property(EndpointProperty.Host)}:29092,PLAINTEXT_HOST://{primaryEndpoint.Property(EndpointProperty.HostAndPort)},PLAINTEXT_INTERNAL://{internalEndpoint.Property(EndpointProperty.HostAndPort)}");
219220

220221
context.EnvironmentVariables["KAFKA_ADVERTISED_LISTENERS"] = advertisedListeners;

src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,71 +7,25 @@ namespace Aspire.Hosting.ApplicationModel;
77

88
internal class ExpressionResolver(string containerHostName, CancellationToken cancellationToken, bool sourceIsContainer)
99
{
10-
class HostAndPortPresence
11-
{
12-
public bool HasHost { get; set; }
13-
public bool HasPort { get; set; }
14-
}
15-
16-
// For each endpoint, keep track of whether host and port are in use
17-
// The key is the unique name of the endpoint, which is the resource name and endpoint name
18-
readonly Dictionary<string, HostAndPortPresence> _endpointUsage = [];
19-
static string EndpointUniqueName(EndpointReference endpointReference) => $"{endpointReference.Resource.Name}/{endpointReference.EndpointName}";
20-
21-
// This marks whether we are in the preprocess phase or not
22-
// Not thread-safe, but we doesn't matter, since this class is never used concurrently
23-
bool Preprocess { get; set; }
2410

2511
async Task<string?> EvalEndpointAsync(EndpointReference endpointReference, EndpointProperty property)
2612
{
27-
var endpointUniqueName = EndpointUniqueName(endpointReference);
28-
29-
// In the preprocess phase, our only goal is to determine if the host and port properties are both used
30-
// for each endpoint.
31-
if (Preprocess)
32-
{
33-
if (!_endpointUsage.TryGetValue(endpointUniqueName, out var hostAndPortPresence))
34-
{
35-
hostAndPortPresence = new HostAndPortPresence();
36-
_endpointUsage[endpointUniqueName] = hostAndPortPresence;
37-
}
38-
39-
if (property is EndpointProperty.Host or EndpointProperty.IPV4Host)
40-
{
41-
hostAndPortPresence.HasHost = true;
42-
}
43-
else if (property == EndpointProperty.Port)
44-
{
45-
hostAndPortPresence.HasPort = true;
46-
}
47-
else if (property is EndpointProperty.Url or EndpointProperty.HostAndPort)
48-
{
49-
hostAndPortPresence.HasHost = hostAndPortPresence.HasPort = true;
50-
}
51-
return string.Empty;
52-
}
5313
// We need to use the root resource, e.g. AzureStorageResource instead of AzureBlobResource
5414
// Otherwise, we get the wrong values for IsContainer and Name
5515
var target = endpointReference.Resource.GetRootResource();
5616

57-
bool HasBothHostAndPort() =>
58-
_endpointUsage[endpointUniqueName].HasHost &&
59-
_endpointUsage[endpointUniqueName].HasPort;
60-
61-
return (property, target.IsContainer(), HasBothHostAndPort()) switch
17+
return (property, target.IsContainer()) switch
6218
{
6319
// If Container -> Container, we go directly to the container name and target port, bypassing the host
64-
// But only do this if we have processed both the host and port properties for that same endpoint.
65-
// This allows the host and port to be handled in a unified way.
66-
(EndpointProperty.Host or EndpointProperty.IPV4Host, true, true) => target.Name,
67-
(EndpointProperty.Port, true, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false),
20+
(EndpointProperty.Host or EndpointProperty.IPV4Host, true) => target.Name,
21+
(EndpointProperty.Port, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false),
6822
// If Container -> Exe, we need to go through the container host
69-
(EndpointProperty.Host or EndpointProperty.IPV4Host, false, _) => containerHostName,
70-
(EndpointProperty.Url, _, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}",
23+
(EndpointProperty.Host or EndpointProperty.IPV4Host, false) => containerHostName,
24+
(EndpointProperty.Url, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}",
7125
endpointReference.Scheme,
7226
await EvalEndpointAsync(endpointReference, EndpointProperty.Host).ConfigureAwait(false),
7327
await EvalEndpointAsync(endpointReference, EndpointProperty.Port).ConfigureAwait(false)),
74-
(EndpointProperty.HostAndPort, _, _) => string.Format(CultureInfo.InvariantCulture, "{0}:{1}",
28+
(EndpointProperty.HostAndPort, _) => string.Format(CultureInfo.InvariantCulture, "{0}:{1}",
7529
await EvalEndpointAsync(endpointReference, EndpointProperty.Host).ConfigureAwait(false),
7630
await EvalEndpointAsync(endpointReference, EndpointProperty.Port).ConfigureAwait(false)),
7731
_ => await endpointReference.Property(property).GetValueAsync(cancellationToken).ConfigureAwait(false)
@@ -159,8 +113,8 @@ async Task<ResolvedValue> ResolveConnectionStringReferenceAsync(ConnectionString
159113
// so we need to do the same here.
160114
var value = await ResolveInternalAsync(cs.Resource.ConnectionStringExpression).ConfigureAwait(false);
161115

162-
// While pre-processing the endpoints, we never throw
163-
if (!Preprocess && string.IsNullOrEmpty(value.Value) && !cs.Optional)
116+
// Throw if the connection string is required but not present
117+
if (string.IsNullOrEmpty(value.Value) && !cs.Optional)
164118
{
165119
cs.ThrowConnectionStringUnavailableException();
166120
}
@@ -188,12 +142,6 @@ async ValueTask<ResolvedValue> ResolveInternalAsync(object? value)
188142
static async ValueTask<ResolvedValue> ResolveWithContainerSourceAsync(IValueProvider valueProvider, string containerHostName, bool sourceIsContainer, CancellationToken cancellationToken)
189143
{
190144
var resolver = new ExpressionResolver(containerHostName, cancellationToken, sourceIsContainer);
191-
192-
// Run the processing phase to know if the host and port properties are both used for each endpoint.
193-
resolver.Preprocess = true;
194-
await resolver.ResolveInternalAsync(valueProvider).ConfigureAwait(false);
195-
resolver.Preprocess = false;
196-
197145
return await resolver.ResolveInternalAsync(valueProvider).ConfigureAwait(false);
198146
}
199147

tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@ public record ExpressionResolverTestData(bool SourceIsContainer, IValueProvider
6363
[InlineData("Url2", true, false, "Url=http://ContainerHostName:12345;")]
6464
[InlineData("Url2", true, true, "Url=http://testresource:10000;")]
6565
[InlineData("OnlyHost", true, false, "Host=ContainerHostName;")]
66-
[InlineData("OnlyHost", true, true, "Host=localhost;")] // host not replaced since no port
66+
[InlineData("OnlyHost", true, true, "Host=testresource;")] // host now replaced to container name
6767
[InlineData("OnlyPort", true, false, "Port=12345;")]
68-
[InlineData("OnlyPort", true, true, "Port=12345;")] // port not replaced since no host
68+
[InlineData("OnlyPort", true, true, "Port=10000;")] // port now replaced with target port
6969
[InlineData("HostAndPort", true, false, "HostPort=ContainerHostName:12345")]
7070
[InlineData("HostAndPort", true, true, "HostPort=testresource:10000")] // host not replaced since no port
7171
[InlineData("PortBeforeHost", true, false, "Port=12345;Host=ContainerHostName;")]
7272
[InlineData("PortBeforeHost", true, true, "Port=10000;Host=testresource;")]
7373
[InlineData("FullAndPartial", true, false, "Test1=http://ContainerHostName:12345/;Test2=https://localhost:12346/;")]
74-
[InlineData("FullAndPartial", true, true, "Test1=http://testresource:10000/;Test2=https://localhost:12346/;")] // Second port not replaced since host is hard coded
74+
[InlineData("FullAndPartial", true, true, "Test1=http://testresource:10000/;Test2=https://localhost:10001/;")]
7575
public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprName, bool sourceIsContainer, bool targetIsContainer, string expectedConnectionString)
7676
{
7777
var builder = DistributedApplication.CreateBuilder();

tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ public async Task EnvironmentVariableExpressions()
242242

243243
Assert.Equal(4, config.Count);
244244
Assert.Equal($"http://container1:10005/foo", config["URL"]);
245-
Assert.Equal("90", config["PORT"]);
245+
Assert.Equal("10005", config["PORT"]);
246246
Assert.Equal("10005", config["TARGET_PORT"]);
247247
Assert.Equal("connectionString;name=1", config["HOST"]);
248248

0 commit comments

Comments
 (0)