Skip to content

Commit a1f0a59

Browse files
authored
Set HTTP/HTTPS_PORTS on projects in publish mode (dotnet#4404)
* Set HTTP/HTTPS_PORTS on projects in publish mode - Only do this for explicit endpoints, not from profile Fixes dotnet#3749
1 parent 900eb72 commit a1f0a59

File tree

7 files changed

+178
-32
lines changed

7 files changed

+178
-32
lines changed

playground/TestShop/AppHost/aspire-manifest.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"basketcache": {
2828
"type": "container.v0",
2929
"connectionString": "{basketcache.bindings.tcp.host}:{basketcache.bindings.tcp.port}",
30-
"image": "docker.io/library/redis:7.2.4",
30+
"image": "docker.io/library/redis:7.2",
3131
"args": [
3232
"--save",
3333
"60",
@@ -57,6 +57,7 @@
5757
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
5858
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
5959
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
60+
"HTTP_PORTS": "{catalogservice.bindings.http.targetPort}",
6061
"ConnectionStrings__catalogdb": "{catalogdb.connectionString}"
6162
},
6263
"bindings": {
@@ -75,7 +76,7 @@
7576
"messaging": {
7677
"type": "container.v0",
7778
"connectionString": "amqp://guest:{rabbitmq-password.value}@{messaging.bindings.tcp.host}:{messaging.bindings.tcp.port}",
78-
"image": "docker.io/library/rabbitmq:3-management",
79+
"image": "docker.io/library/rabbitmq:3.13-management",
7980
"volumes": [
8081
{
8182
"name": "TestShop.AppHost-messaging-data",
@@ -110,6 +111,7 @@
110111
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
111112
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
112113
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
114+
"HTTP_PORTS": "{basketservice.bindings.http.targetPort}",
113115
"ConnectionStrings__basketcache": "{basketcache.connectionString}",
114116
"ConnectionStrings__messaging": "{messaging.connectionString}"
115117
},
@@ -134,6 +136,7 @@
134136
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
135137
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
136138
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
139+
"HTTP_PORTS": "{frontend.bindings.http.targetPort}",
137140
"services__basketservice__http__0": "{basketservice.bindings.http.url}",
138141
"services__basketservice__https__0": "{basketservice.bindings.https.url}",
139142
"services__catalogservice__http__0": "{catalogservice.bindings.http.url}",
@@ -172,6 +175,7 @@
172175
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
173176
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
174177
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
178+
"HTTP_PORTS": "{apigateway.bindings.http.targetPort}",
175179
"services__basketservice__http__0": "{basketservice.bindings.http.url}",
176180
"services__basketservice__https__0": "{basketservice.bindings.https.url}",
177181
"services__catalogservice__http__0": "{catalogservice.bindings.http.url}",
@@ -198,6 +202,7 @@
198202
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
199203
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
200204
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
205+
"HTTP_PORTS": "{catalogdbapp.bindings.http.targetPort}",
201206
"ConnectionStrings__catalogdb": "{catalogdb.connectionString}"
202207
},
203208
"bindings": {
@@ -245,4 +250,4 @@
245250
}
246251
}
247252
}
248-
}
253+
}

src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ public string Transport
130130
/// </summary>
131131
internal bool FromLaunchProfile { get; set; }
132132

133+
/// <summary>
134+
/// Gets or sets a value indicating whether to skip adding the endpoint from HTTPS_PORTS env var
135+
/// </summary>
136+
internal bool ExcludeFromPortEnvironment { get; set; }
137+
133138
/// <summary>
134139
/// The environment variable that contains the target port. Setting prevents a variable from flowing into ASPNETCORE_URLS for project resources.
135140
/// </summary>

src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs

+72-23
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.Diagnostics;
45
using Aspire.Hosting.ApplicationModel;
56
using Aspire.Hosting.Dashboard;
67
using Aspire.Hosting.Utils;
@@ -324,30 +325,40 @@ private static IResourceBuilder<ProjectResource> WithProjectDefaults(this IResou
324325
}
325326
else
326327
{
328+
// Set HTTP_PORTS/HTTPS_PORTS in publish mode, to override the default port set in the base image. Note that:
329+
// - We don't set them if we have Kestrel endpoints configured, as Kestrel will get everything from its config.
330+
// - We only do that for endpoint set explicitly (.WithHttpEndpoint), not for the ones coming from launch profile.
331+
// This is because launch profile endpoints are not meant to be used in production.
332+
if (!kestrelEndpointsByScheme.Any())
333+
{
334+
builder.SetBothPortsEnvVariables();
335+
}
336+
327337
// If we aren't a web project (looking at both launch profile and Kestrel config) we don't automatically add bindings.
328338
if (launchProfile?.ApplicationUrl == null && !kestrelEndpointsByScheme.Any())
329339
{
330340
return builder;
331341
}
332342

333-
if (!projectResource.Annotations.OfType<EndpointAnnotation>().Any(sb => sb.UriScheme == "http" || string.Equals(sb.Name, "http", StringComparisons.EndpointAnnotationName)))
343+
string[] schemes = ["http", "https"];
344+
foreach (var scheme in schemes)
334345
{
335-
builder.WithEndpoint("http", e =>
346+
if (!projectResource.Annotations.OfType<EndpointAnnotation>().Any(sb => sb.UriScheme == scheme || string.Equals(sb.Name, scheme, StringComparisons.EndpointAnnotationName)))
336347
{
337-
e.UriScheme = "http";
338-
e.Transport = adjustTransport(e);
339-
},
340-
createIfNotExists: true);
341-
}
348+
builder.WithEndpoint(scheme, e =>
349+
{
350+
e.UriScheme = scheme;
351+
e.Transport = adjustTransport(e);
342352

343-
if (!projectResource.Annotations.OfType<EndpointAnnotation>().Any(sb => sb.UriScheme == "https" || string.Equals(sb.Name, "https", StringComparisons.EndpointAnnotationName)))
344-
{
345-
builder.WithEndpoint("https", e =>
346-
{
347-
e.UriScheme = "https";
348-
e.Transport = adjustTransport(e);
349-
},
350-
createIfNotExists: true);
353+
// In the https case, we don't want this default endpoint to end up in the HTTPS_PORTS env var,
354+
// because the container likely won't be set up to listen on https (e.g. ACA case)
355+
if (scheme == "https")
356+
{
357+
e.ExcludeFromPortEnvironment = true;
358+
}
359+
},
360+
createIfNotExists: true);
361+
}
351362
}
352363
}
353364

@@ -440,13 +451,11 @@ private static IConfiguration GetConfiguration(ProjectResource projectResource)
440451
return configBuilder.Build();
441452
}
442453

454+
static bool IsValidAspNetCoreUrl(EndpointAnnotation e) =>
455+
e.UriScheme is "http" or "https" && e.TargetPortEnvironmentVariable is null;
456+
443457
private static void SetAspNetCoreUrls(this IResourceBuilder<ProjectResource> builder)
444458
{
445-
if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
446-
{
447-
return;
448-
}
449-
450459
builder.WithEnvironment(context =>
451460
{
452461
if (context.EnvironmentVariables.ContainsKey("ASPNETCORE_URLS"))
@@ -460,9 +469,6 @@ private static void SetAspNetCoreUrls(this IResourceBuilder<ProjectResource> bui
460469
var processedHttpsPort = false;
461470
var first = true;
462471

463-
static bool IsValidAspNetCoreUrl(EndpointAnnotation e) =>
464-
e.UriScheme is "http" or "https" && e.TargetPortEnvironmentVariable is null;
465-
466472
// Turn http and https endpoints into a single ASPNETCORE_URLS environment variable.
467473
foreach (var e in builder.Resource.GetEndpoints().Where(e => IsValidAspNetCoreUrl(e.EndpointAnnotation)))
468474
{
@@ -491,4 +497,47 @@ static bool IsValidAspNetCoreUrl(EndpointAnnotation e) =>
491497
}
492498
});
493499
}
500+
501+
private static void SetBothPortsEnvVariables(this IResourceBuilder<ProjectResource> builder)
502+
{
503+
builder.WithEnvironment(context =>
504+
{
505+
builder.SetOnePortsEnvVariable(context, "HTTP_PORTS", "http");
506+
builder.SetOnePortsEnvVariable(context, "HTTPS_PORTS", "https");
507+
});
508+
}
509+
510+
private static void SetOnePortsEnvVariable(this IResourceBuilder<ProjectResource> builder, EnvironmentCallbackContext context, string portEnvVariable, string scheme)
511+
{
512+
if (context.EnvironmentVariables.ContainsKey(portEnvVariable))
513+
{
514+
// If the user has already set that variable, we don't want to override it.
515+
return;
516+
}
517+
518+
var ports = new ReferenceExpressionBuilder();
519+
var firstPort = true;
520+
521+
// Turn endpoint ports into a single environment variable
522+
foreach (var e in builder.Resource.GetEndpoints().Where(e => IsValidAspNetCoreUrl(e.EndpointAnnotation)))
523+
{
524+
if (e.EndpointAnnotation.UriScheme == scheme && !e.EndpointAnnotation.ExcludeFromPortEnvironment)
525+
{
526+
Debug.Assert(!e.EndpointAnnotation.FromLaunchProfile, "Endpoints from launch profile should never make it here");
527+
528+
if (!firstPort)
529+
{
530+
ports.AppendLiteral(";");
531+
}
532+
533+
ports.Append($"{e.Property(EndpointProperty.TargetPort)}");
534+
firstPort = false;
535+
}
536+
}
537+
538+
if (!firstPort)
539+
{
540+
context.EnvironmentVariables[portEnvVariable] = ports.Build();
541+
}
542+
}
494543
}

tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,8 @@ public void VerifyTestProgramFullManifest()
424424
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
425425
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
426426
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
427-
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true"
427+
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
428+
"HTTP_PORTS": "{servicea.bindings.http.targetPort}"
428429
},
429430
"bindings": {
430431
"http": {
@@ -446,7 +447,8 @@ public void VerifyTestProgramFullManifest()
446447
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
447448
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
448449
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
449-
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true"
450+
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
451+
"HTTP_PORTS": "{serviceb.bindings.http.targetPort}"
450452
},
451453
"bindings": {
452454
"http": {
@@ -500,6 +502,7 @@ public void VerifyTestProgramFullManifest()
500502
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
501503
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
502504
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
505+
"HTTP_PORTS": "{integrationservicea.bindings.http.targetPort}",
503506
"SKIP_RESOURCES": "None",
504507
"ConnectionStrings__tempdb": "{tempdb.connectionString}",
505508
"ConnectionStrings__mysqldb": "{mysqldb.connectionString}",

tests/Aspire.Hosting.Tests/ProjectResourceTests.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public async Task AspNetCoreUrlsNotInjectedInPublishMode()
273273

274274
var resource = Assert.Single(projectResources);
275275

276-
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource);
276+
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Publish);
277277

278278
Assert.False(config.ContainsKey("ASPNETCORE_URLS"));
279279
Assert.False(config.ContainsKey("ASPNETCORE_HTTPS_PORT"));
@@ -413,7 +413,8 @@ public async Task VerifyManifest(bool disableForwardedHeaders)
413413
"env": {
414414
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
415415
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
416-
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory"{{fordwardedHeadersEnvVar}}
416+
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory"{{fordwardedHeadersEnvVar}},
417+
"HTTP_PORTS": "{projectName.bindings.http.targetPort}"
417418
},
418419
"bindings": {
419420
"http": {
@@ -463,7 +464,8 @@ public async Task VerifyManifestWithArgs()
463464
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
464465
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
465466
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
466-
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true"
467+
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
468+
"HTTP_PORTS": "{projectName.bindings.http.targetPort}"
467469
},
468470
"bindings": {
469471
"http": {

tests/Aspire.Hosting.Tests/WithEndpointTests.cs

+36-1
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,9 @@ public async Task VerifyManifestProjectWithHttpEndpointDoesNotAllocatePort()
470470
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
471471
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
472472
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
473-
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true"
473+
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
474+
"HTTP_PORTS": "{proj.bindings.hp.targetPort}",
475+
"HTTPS_PORTS": "{proj.bindings.hps.targetPort}"
474476
},
475477
"bindings": {
476478
"hp": {
@@ -490,6 +492,39 @@ public async Task VerifyManifestProjectWithHttpEndpointDoesNotAllocatePort()
490492
Assert.Equal(expectedManifest, manifest.ToString());
491493
}
492494

495+
[Fact]
496+
public async Task VerifyManifestProjectWithEndpointsSetsPortsEnvVariables()
497+
{
498+
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
499+
var project = builder.AddProject<TestProject>("proj")
500+
.WithHttpEndpoint()
501+
.WithHttpEndpoint(name: "hp1", port: 5001)
502+
.WithHttpEndpoint(name: "hp2", port: 5002, targetPort: 5003)
503+
.WithHttpEndpoint(name: "hp3", targetPort: 5004)
504+
.WithHttpEndpoint(name: "hp4")
505+
.WithHttpsEndpoint()
506+
.WithHttpsEndpoint(name: "hps1", port: 7001)
507+
.WithHttpsEndpoint(name: "hps2", port: 7002, targetPort: 7003)
508+
.WithHttpsEndpoint(name: "hps3", targetPort: 7004)
509+
.WithHttpsEndpoint(name: "hps4", targetPort: 7005);
510+
511+
var manifest = await ManifestUtils.GetManifest(project.Resource);
512+
513+
var expectedEnv =
514+
"""
515+
{
516+
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
517+
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
518+
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
519+
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
520+
"HTTP_PORTS": "{proj.bindings.http.targetPort};{proj.bindings.hp1.targetPort};{proj.bindings.hp2.targetPort};{proj.bindings.hp3.targetPort};{proj.bindings.hp4.targetPort}",
521+
"HTTPS_PORTS": "{proj.bindings.https.targetPort};{proj.bindings.hps1.targetPort};{proj.bindings.hps2.targetPort};{proj.bindings.hps3.targetPort};{proj.bindings.hps4.targetPort}"
522+
}
523+
""";
524+
525+
Assert.Equal(expectedEnv, manifest["env"]!.ToString());
526+
}
527+
493528
[Fact]
494529
public async Task VerifyManifestPortAllocationIsGlobal()
495530
{

tests/eventhubns.module.bicep

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
targetScope = 'resourceGroup'
2+
3+
@description('')
4+
param location string = resourceGroup().location
5+
6+
@description('')
7+
param sku string = 'Standard'
8+
9+
@description('')
10+
param principalId string
11+
12+
@description('')
13+
param principalType string
14+
15+
16+
resource eventHubsNamespace_wORIGuvCQ 'Microsoft.EventHub/namespaces@2021-11-01' = {
17+
name: toLower(take('eventhubns${uniqueString(resourceGroup().id)}', 24))
18+
location: location
19+
tags: {
20+
'aspire-resource-name': 'eventhubns'
21+
}
22+
sku: {
23+
name: sku
24+
}
25+
properties: {
26+
}
27+
}
28+
29+
resource roleAssignment_2so8CKuFt 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
30+
scope: eventHubsNamespace_wORIGuvCQ
31+
name: guid(eventHubsNamespace_wORIGuvCQ.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec'))
32+
properties: {
33+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f526a384-b230-433a-b45c-95f59c4a2dec')
34+
principalId: principalId
35+
principalType: principalType
36+
}
37+
}
38+
39+
resource eventHub_4BpPMTltx 'Microsoft.EventHub/namespaces/eventhubs@2021-11-01' = {
40+
parent: eventHubsNamespace_wORIGuvCQ
41+
name: 'hub'
42+
location: location
43+
properties: {
44+
}
45+
}
46+
47+
output eventHubsEndpoint string = eventHubsNamespace_wORIGuvCQ.properties.serviceBusEndpoint

0 commit comments

Comments
 (0)