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
6 changes: 5 additions & 1 deletion src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,11 @@ private void EnsureProxylessEndpointPort(IResource resource, EndpointAnnotation

private static bool NeedsPublicPort(IResource resource, EndpointAnnotation endpoint)
{
return !endpoint.IsProxied && !TryGetEffectiveFixedPublicPort(resource, endpoint, randomizePorts: false, out _);
// DCP can allocate a port only for resources it launches as workloads. This includes compute
// resources and annotation-backed containers; integration-owned endpoints publish their own addresses.
return (resource is IComputeResource || resource.IsContainer()) &&
!endpoint.IsProxied &&
!TryGetEffectiveFixedPublicPort(resource, endpoint, randomizePorts: false, out _);
Comment thread
karolz-ms marked this conversation as resolved.
}

private int? TryGetPersistedProxylessEndpointPort(IResource resource, EndpointAnnotation endpoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,61 @@ public async Task ShowTunnelUrlsCommand_UsesTargetEndpointNetworkContext()
interaction.Message);
}

[Fact]
public async Task DcpStartupPublishesDevTunnelUrls()
{
const int targetPort = 3000;
const string tunnelUrl = "https://n4skq32k-3000.use.devtunnels.ms";
const string inspectUrl = "https://n4skq32k-3000-inspect.use.devtunnels.ms";
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
Comment thread
danegsta marked this conversation as resolved.
var client = new TestDevTunnelClient
{
TunnelStatus = new("mytunnel", HostConnections: 1, ClientConnections: 0, Description: "", Labels: [])
{
Ports =
[
new(targetPort, "http")
{
PortUri = new Uri($"{tunnelUrl}/")
}
]
}
};
var (command, arguments) = GetLongRunningCommand();
using var builder = TestDistributedApplicationBuilder.Create();
builder.Configuration["ASPIRE_DEVTUNNEL_CLI_PATH"] = command;
builder.Services.AddSingleton<IDevTunnelClient>(client);
builder.Services.AddSingleton<IRequiredCommandValidator, TestRequiredCommandValidator>();

var target = builder.AddExecutable("target", command, Environment.CurrentDirectory, arguments)
.WithHttpEndpoint(targetPort: targetPort, name: "http");
var tunnel = builder.AddDevTunnel("tunnel", "mytunnel")
.WithReference(target);
var tunnelPort = Assert.Single(tunnel.Resource.Ports);
foreach (var annotation in tunnel.Resource.Annotations.OfType<CommandLineArgsCallbackAnnotation>().ToArray())
{
tunnel.Resource.Annotations.Remove(annotation);
}
tunnel.WithArgs(arguments);

using var app = builder.Build();

var startTask = app.StartAsync(cts.Token);
var resourceEvent = await app.ResourceNotifications.WaitForResourceAsync(
tunnelPort.Name,
e => e.Snapshot.State?.Text == KnownResourceStates.Running &&
e.Snapshot.Urls.Any(u => u.Url == tunnelUrl && !u.IsInactive) &&
e.Snapshot.Urls.Any(u => u.Url == inspectUrl && !u.IsInactive),
cts.Token);
await startTask;

Assert.Equal("n4skq32k-3000.use.devtunnels.ms", tunnelPort.TunnelEndpointAnnotation.AllocatedEndpoint?.Address);
Assert.Contains(resourceEvent.Snapshot.Urls, u => u.Url == tunnelUrl && !u.IsInactive);
Assert.Contains(resourceEvent.Snapshot.Urls, u => u.Url == inspectUrl && !u.IsInactive);

await app.StopAsync(cts.Token);
}

[Fact]
public async Task ResourceReady_PublishesUrlProperties()
{
Expand Down Expand Up @@ -636,6 +691,15 @@ DevTunnelPortResource.InspectUrlPropertyName or
});
}

private static (string Command, string[] Arguments) GetLongRunningCommand()
{
// Windows has no sleep executable, and `timeout` exits immediately when stdin is redirected.
// Loopback ping is dependency-free and remains bounded if test cleanup is interrupted.
return OperatingSystem.IsWindows()
? ("cmd.exe", ["/c", "ping", "-n", "180", "127.0.0.1"])
: ("sleep", ["180"]);
}

private sealed class ProjectA : IProjectMetadata
{
public string ProjectPath => "projectA";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,15 @@ public void AddDotnetProject_UsesDotnetCommandAndProjectDirectoryAsWorkingDirect
}

[Fact]
public void AddDotnetProject_ResourceSupportsServiceDiscovery()
public void AddDotnetProject_ResourceSupportsServiceDiscoveryAndIsComputeResource()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);

var app = builder.AddDotnetProject("svc", "MyService.csproj", o => o.ExcludeLaunchProfile = true);

Assert.IsAssignableFrom<IResourceWithServiceDiscovery>(app.Resource);
Assert.IsAssignableFrom<ExecutableResource>(app.Resource);
Assert.IsAssignableFrom<IComputeResource>(app.Resource);
}

[Fact]
Expand Down
84 changes: 84 additions & 0 deletions tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Aspire.Dashboard.Model;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Dcp.Model;
using Aspire.Hosting.DevTunnels;
using Aspire.Hosting.Diagnostics;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Tests.Utils;
Expand Down Expand Up @@ -1051,6 +1052,88 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessNoPortNoTargetPor
Assert.Equal(allocatedPort, int.Parse(envVarVal, CultureInfo.InvariantCulture));
}

[Fact]
public async Task ProxylessPortAllocatorOnlyAllocatesPortsForDcpWorkloads()
{
var (rangeStart, rangeEnd) = GetAvailableConsecutivePortPair();
var builder = DistributedApplication.CreateBuilder();

var compute = builder.AddExecutable("compute", "compute", Environment.CurrentDirectory)
Comment thread
karolz-ms marked this conversation as resolved.
.WithEndpoint(name: "tcp", isProxied: false);
var target = builder.AddExecutable("target", "target", Environment.CurrentDirectory)
.WithHttpEndpoint(targetPort: 8000, name: "http");
builder.AddDevTunnel("tunnel")
.WithReference(target);

var dcpOptions = new DcpOptions
{
DashboardPath = "./dashboard",
ProxylessEndpointPortRangeStart = rangeStart,
ProxylessEndpointPortRangeEnd = rangeEnd
};
var kubernetesService = new TestKubernetesService();
using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var tunnelPort = Assert.Single(distributedAppModel.Resources.OfType<DevTunnelPortResource>());
var computeEndpoint = compute.GetEndpoint("tcp").EndpointAnnotation;
var tunnelEndpoint = Assert.Single(tunnelPort.Annotations.OfType<EndpointAnnotation>());
var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions);

await appExecutor.RunApplicationAsync();

Assert.NotNull(computeEndpoint.AllocatedEndpoint);
var computePort = Assert.IsType<int>(computeEndpoint.Port);
Assert.InRange(computePort, rangeStart, rangeEnd);
Assert.Equal(computePort, computeEndpoint.TargetPort);
Assert.Null(tunnelEndpoint.Port);
Assert.Null(tunnelEndpoint.TargetPort);
Assert.Null(tunnelEndpoint.AllocatedEndpoint);
Comment thread
karolz-ms marked this conversation as resolved.
}

[Fact]
public async Task ProxylessPortAllocatorAllocatesPortForNonComputeContainerResource()
{
const int targetPort = 10000;
var (allocatedPort, _) = GetAvailableConsecutivePortPair();
var builder = DistributedApplication.CreateBuilder();

var emulator = builder.AddResource(new TestContainerResource("emulator"))
.WithAnnotation(new ContainerImageAnnotation { Image = "image" })
.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = ContainerLifetime.Persistent })
.WithHttpEndpoint(targetPort: targetPort, name: "http");

var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
})
.Build();
var dcpOptions = new DcpOptions
{
DashboardPath = "./dashboard",
ProxylessEndpointPortRangeStart = allocatedPort,
ProxylessEndpointPortRangeEnd = allocatedPort
};
var kubernetesService = new TestKubernetesService();
using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var endpoint = emulator.GetEndpoint("http").EndpointAnnotation;
var appExecutor = CreateAppExecutor(
distributedAppModel,
configuration: configuration,
kubernetesService: kubernetesService,
dcpOptions: dcpOptions);

await appExecutor.RunApplicationAsync();

Assert.IsNotAssignableFrom<IComputeResource>(emulator.Resource);
Assert.True(emulator.Resource.IsContainer());
Assert.Equal(allocatedPort, endpoint.Port);
Assert.Equal(targetPort, endpoint.TargetPort);
Assert.Equal(allocatedPort, endpoint.AllocatedEndpoint?.Port);
Assert.Single(kubernetesService.CreatedResources.OfType<Container>(), c => c.AppModelResourceName == emulator.Resource.Name);
}

[Fact]
public async Task ProxylessPortAllocatorExcludesFixedPublicPorts()
{
Expand Down Expand Up @@ -10208,6 +10291,7 @@ private static X509Certificate2 CreateTestCertificate()

private sealed class TestExecutableResource(string directory) : ExecutableResource("TestExecutable", "test", directory);
private sealed class TestOtherExecutableResource(string directory) : ExecutableResource("TestOtherExecutable", "test-other", directory);
private sealed class TestContainerResource(string name) : Resource(name), IResourceWithEndpoints;

private sealed class NullValueProvider : IValueProvider
{
Expand Down
Loading