Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Aspire.Cli/Aspire.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(SharedDir)AppHostProfilePortGenerator.cs" Link="Utils\AppHostProfilePortGenerator.cs" />
<Compile Include="$(SharedDir)BundleDiscovery.cs" Link="Layout\BundleDiscovery.cs" />
<Compile Include="$(SharedDir)EnumerableExtensions.cs" Link="Utils\EnumerableExtensions.cs" />
<Compile Include="$(SharedDir)KnownConfigNames.cs" Link="Utils\KnownConfigNames.cs" />
Expand Down
21 changes: 8 additions & 13 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Aspire.Shared;

namespace Aspire.Cli.Commands;

Expand Down Expand Up @@ -398,32 +399,26 @@ private int DropAspireConfig(DirectoryInfo directory, string appHostPath, string
// Normally scaffolding + codegen creates these, but our thin init skips scaffolding.
if (settings["profiles"] is null)
{
// Port ranges match CliTemplateFactory.GenerateRandomPorts()
var httpPort = Random.Shared.Next(15000, 15300);
var httpsPort = Random.Shared.Next(17000, 17300);
var otlpHttpPort = Random.Shared.Next(19000, 19300);
var otlpHttpsPort = Random.Shared.Next(21000, 21300);
var resourceHttpPort = Random.Shared.Next(20000, 20300);
var resourceHttpsPort = Random.Shared.Next(22000, 22300);
var ports = AppHostProfilePortGenerator.Generate(Random.Shared);

settings["profiles"] = new JsonObject
{
["https"] = new JsonObject
{
["applicationUrl"] = $"https://localhost:{httpsPort};http://localhost:{httpPort}",
["applicationUrl"] = $"https://localhost:{ports.DashboardHttpsPort};http://localhost:{ports.DashboardHttpPort}",
["environmentVariables"] = new JsonObject
{
["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{otlpHttpsPort}",
["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{resourceHttpsPort}"
["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{ports.OtlpHttpsPort}",
["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{ports.ResourceServiceHttpsPort}"
}
},
["http"] = new JsonObject
{
["applicationUrl"] = $"http://localhost:{httpPort}",
["applicationUrl"] = $"http://localhost:{ports.DashboardHttpPort}",
["environmentVariables"] = new JsonObject
{
["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{otlpHttpPort}",
["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{resourceHttpPort}",
["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{ports.OtlpHttpPort}",
["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{ports.ResourceServiceHttpPort}",
["ASPIRE_ALLOW_UNSECURED_TRANSPORT"] = "true"
}
}
Expand Down
26 changes: 8 additions & 18 deletions src/Aspire.Cli/Templating/CliTemplateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Aspire.Cli.Resources;
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Utils;
using Aspire.Shared;
using Microsoft.Extensions.Logging;

namespace Aspire.Cli.Templating;
Expand Down Expand Up @@ -164,37 +165,26 @@ private bool IsTemplateAvailable(ITemplate template)
return _languageDiscovery.GetLanguageById(new LanguageId(template.LanguageId)) is not null;
}

private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, TemplatePorts ports, string hostName = "localhost")
private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, AppHostProfilePorts ports, string hostName = "localhost")
{
return content
.Replace("{{projectName}}", projectName)
.Replace("{{projectNameLower}}", projectNameLower)
.Replace("{{aspireVersion}}", aspireVersion)
.Replace("{{hostName}}", hostName)
.Replace("{{httpPort}}", ports.HttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{httpsPort}}", ports.HttpsPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{httpPort}}", ports.DashboardHttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{httpsPort}}", ports.DashboardHttpsPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{otlpHttpPort}}", ports.OtlpHttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{otlpHttpsPort}}", ports.OtlpHttpsPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{resourceHttpPort}}", ports.ResourceHttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{resourceHttpsPort}}", ports.ResourceHttpsPort.ToString(CultureInfo.InvariantCulture));
.Replace("{{resourceHttpPort}}", ports.ResourceServiceHttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{resourceHttpsPort}}", ports.ResourceServiceHttpsPort.ToString(CultureInfo.InvariantCulture));
}

private static TemplatePorts GenerateRandomPorts()
private static AppHostProfilePorts GenerateRandomPorts()
{
return new TemplatePorts(
HttpPort: Random.Shared.Next(15000, 15300),
HttpsPort: Random.Shared.Next(17000, 17300),
OtlpHttpPort: Random.Shared.Next(19000, 19300),
OtlpHttpsPort: Random.Shared.Next(21000, 21300),
ResourceHttpPort: Random.Shared.Next(20000, 20300),
ResourceHttpsPort: Random.Shared.Next(22000, 22300));
return AppHostProfilePortGenerator.Generate(Random.Shared);
}

private sealed record TemplatePorts(
int HttpPort, int HttpsPort,
int OtlpHttpPort, int OtlpHttpsPort,
int ResourceHttpPort, int ResourceHttpsPort);

private static void AddOptionIfMissing(System.CommandLine.Command command, System.CommandLine.Option option)
{
if (!command.Options.Contains(option))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(SharedDir)AppHostProfilePortGenerator.cs" Link="AppHostProfilePortGenerator.cs" />
<Compile Include="$(SharedDir)NpmVersionHelper.cs" Link="NpmVersionHelper.cs" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,19 +120,16 @@ public Dictionary<string, string> Scaffold(ScaffoldRequest request)
? new Random(request.PortSeed.Value)
: Random.Shared;

var httpsPort = random.Next(10000, 65000);
var httpPort = random.Next(10000, 65000);
var otlpPort = random.Next(10000, 65000);
var resourceServicePort = random.Next(10000, 65000);
var ports = AppHostProfilePortGenerator.Generate(random);

files["apphost.run.json"] = $$"""
{
"profiles": {
"https": {
"applicationUrl": "https://localhost:{{httpsPort}};http://localhost:{{httpPort}}",
"applicationUrl": "https://localhost:{{ports.DashboardHttpsPort}};http://localhost:{{ports.DashboardHttpPort}}",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{otlpPort}}",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{resourceServicePort}}"
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{ports.OtlpHttpsPort}}",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{ports.ResourceServiceHttpsPort}}"
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/Shared/AppHostProfilePortGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Shared;

internal static class AppHostProfilePortGenerator
{
internal const int DashboardHttpPortMin = 15000;
internal const int DashboardHttpPortMaxExclusive = 15300;
internal const int DashboardHttpsPortMin = 17000;
internal const int DashboardHttpsPortMaxExclusive = 17300;
internal const int OtlpHttpPortMin = 19000;
internal const int OtlpHttpPortMaxExclusive = 19300;
internal const int OtlpHttpsPortMin = 21000;
internal const int OtlpHttpsPortMaxExclusive = 21300;
internal const int ResourceServiceHttpPortMin = 20000;
internal const int ResourceServiceHttpPortMaxExclusive = 20300;
internal const int ResourceServiceHttpsPortMin = 22000;
internal const int ResourceServiceHttpsPortMaxExclusive = 22300;

internal static AppHostProfilePorts Generate(Random random)
{
ArgumentNullException.ThrowIfNull(random);

return new AppHostProfilePorts(
DashboardHttpPort: random.Next(DashboardHttpPortMin, DashboardHttpPortMaxExclusive),
DashboardHttpsPort: random.Next(DashboardHttpsPortMin, DashboardHttpsPortMaxExclusive),
OtlpHttpPort: random.Next(OtlpHttpPortMin, OtlpHttpPortMaxExclusive),
OtlpHttpsPort: random.Next(OtlpHttpsPortMin, OtlpHttpsPortMaxExclusive),
ResourceServiceHttpPort: random.Next(ResourceServiceHttpPortMin, ResourceServiceHttpPortMaxExclusive),
ResourceServiceHttpsPort: random.Next(ResourceServiceHttpsPortMin, ResourceServiceHttpsPortMaxExclusive));
}
}

internal readonly record struct AppHostProfilePorts(
int DashboardHttpPort,
int DashboardHttpsPort,
int OtlpHttpPort,
int OtlpHttpsPort,
int ResourceServiceHttpPort,
int ResourceServiceHttpsPort);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json.Nodes;
using Aspire.Shared;
using Aspire.TypeSystem;

namespace Aspire.Hosting.CodeGeneration.TypeScript.Tests;
Expand Down Expand Up @@ -187,6 +188,41 @@ public void Scaffold_DoesNotEmitRootTsConfig_WhenOneAlreadyExists()
Assert.Equal(existingTsConfig, File.ReadAllText(existingTsConfigPath));
}

[Theory]
[InlineData(null)]
[InlineData(0)]
[InlineData(1)]
[InlineData(16626)]
[InlineData(55571)]
public void Scaffold_GeneratesProfilePortsOutsideWindowsEphemeralRange(int? portSeed)
{
using var testDir = new TestTempDirectory();

var files = _languageSupport.Scaffold(new ScaffoldRequest
{
TargetPath = testDir.Path,
ProjectName = "PortsApp",
PortSeed = portSeed
});

var appHostRunJson = ParseJson(files["apphost.run.json"]);
var httpsProfile = appHostRunJson["profiles"]!["https"]!.AsObject();
var applicationUrls = httpsProfile["applicationUrl"]!.GetValue<string>().Split(';', StringSplitOptions.RemoveEmptyEntries);
var environmentVariables = httpsProfile["environmentVariables"]!.AsObject();

Assert.Equal(2, applicationUrls.Length);

var httpsPort = GetPort(applicationUrls.Single(url => url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)));
var httpPort = GetPort(applicationUrls.Single(url => url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)));
var otlpHttpsPort = GetPort(environmentVariables["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]!.GetValue<string>());
var resourceServiceHttpsPort = GetPort(environmentVariables["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]!.GetValue<string>());

AssertPortInRange(httpPort, AppHostProfilePortGenerator.DashboardHttpPortMin, AppHostProfilePortGenerator.DashboardHttpPortMaxExclusive);
AssertPortInRange(httpsPort, AppHostProfilePortGenerator.DashboardHttpsPortMin, AppHostProfilePortGenerator.DashboardHttpsPortMaxExclusive);
AssertPortInRange(otlpHttpsPort, AppHostProfilePortGenerator.OtlpHttpsPortMin, AppHostProfilePortGenerator.OtlpHttpsPortMaxExclusive);
AssertPortInRange(resourceServiceHttpsPort, AppHostProfilePortGenerator.ResourceServiceHttpsPortMin, AppHostProfilePortGenerator.ResourceServiceHttpsPortMaxExclusive);
}

[Fact]
public void GetRuntimeSpec_UsesAppHostSpecificTsConfig()
{
Expand All @@ -198,4 +234,14 @@ public void GetRuntimeSpec_UsesAppHostSpecificTsConfig()
}

private static JsonObject ParseJson(string content) => JsonNode.Parse(content)!.AsObject();

private static int GetPort(string url) => new Uri(url).Port;

private const int WindowsEphemeralPortMin = 49152;

private static void AssertPortInRange(int port, int minInclusive, int maxExclusive)
{
Assert.InRange(port, minInclusive, maxExclusive - 1);
Assert.True(port < WindowsEphemeralPortMin, $"Expected port {port} to be below the Windows ephemeral range.");
}
}
Loading