Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added test infrastructure & fixed the YarpResource for testing, https, etc. #1

Merged
merged 9 commits into from
May 24, 2024
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
16 changes: 15 additions & 1 deletion .github/workflows/_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,21 @@ jobs:
run: dotnet build --configuration Release --no-restore -p:BuildNumber=$BUILD_NUMBER -p:SourceRevisionId=$GITHUB_SHA -p:ContinuousIntegrationBuild=true

- name: Test
run: dotnet test --configuration Release --no-restore --no-build
id: test
# Note that the space after the last double dash (--) is intentional
run: >
dotnet test
--logger console --logger trx --logger html --logger GitHubActions
--results-directory ./TestResults --blame
--
RunConfiguration.CollectSourceInformation=true

- name: Updload artifacts (test results)
if: (success() || steps.test.conclusion == 'failure')
uses: actions/upload-artifact@v4
with:
name: TestResults
path: ./TestResults

# - name: Publish samples
# run: |
Expand Down
17 changes: 7 additions & 10 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,24 @@
<PackageVersion Include="Aspire.Hosting" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="$(AspireVersion)" />

<!-- Microsoft.Extensions -->
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.3.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="$(AspireVersion)" />

<!-- OpenTelemetry -->
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />

<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.1" />
<!-- Misc -->
<PackageVersion Include="Yarp.ReverseProxy" Version="2.1.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />

<!-- Testing -->
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="xunit" Version="2.5.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" />
</ItemGroup>
</Project>
5 changes: 4 additions & 1 deletion samples/YarpResource/YarpResource.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
var app1 = builder.AddProject<Projects.WebApplication1>("app1");
var app2 = builder.AddProject<Projects.WebApplication2>("app2");

var isHttps = builder.Configuration["DOTNET_LAUNCH_PROFILE"] == "https";
var ingressPort = int.TryParse(builder.Configuration["Ingress:Port"], out var port) ? port : (int?)null;

builder.AddYarp("ingress")
.WithHttpEndpoint(port: 8001)
.WithEndpoint(scheme: isHttps ? "https" : "http", port: ingressPort)
.WithReference(app1)
.WithReference(app2)
.LoadFromConfiguration("ReverseProxy");
Expand Down
78 changes: 58 additions & 20 deletions src/Aspirant/Yarp/YarpResource.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using Aspire.Hosting.ApplicationModel;
using System.Linq;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using k8s.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Yarp.ReverseProxy.Configuration;

Expand Down Expand Up @@ -91,12 +95,12 @@ public class YarpResource(string name) : Resource(name), IResourceWithServiceDis
// YARP configuration
internal Dictionary<string, RouteConfig> RouteConfigs { get; } = [];
internal Dictionary<string, ClusterConfig> ClusterConfigs { get; } = [];
internal List<EndpointAnnotation> Endpoints { get; } = [];
internal string? ConfigurationSectionName { get; set; }
}

// This starts up the YARP reverse proxy with the configuration from the resource
internal class YarpResourceLifecyclehook(
IHostEnvironment hostEnvironment,
DistributedApplicationExecutionContext executionContext,
ResourceNotificationService resourceNotificationService,
ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable
Expand All @@ -123,13 +127,12 @@ await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with
State = "Starting"
});

// We don't want to create proxies for yarp resources so remove them
// We don't want to proxy for yarp resources so force endpoints to not proxy
var bindings = yarpResource.Annotations.OfType<EndpointAnnotation>().ToList();

foreach (var b in bindings)
{
yarpResource.Annotations.Remove(b);
yarpResource.Endpoints.Add(b);
b.IsProxied = false;
}
}

Expand All @@ -146,8 +149,12 @@ public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appMo
{
return;
}

var builder = WebApplication.CreateSlimBuilder();
var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions
{
ContentRootPath = hostEnvironment.ContentRootPath,
EnvironmentName = hostEnvironment.EnvironmentName,
WebRootPath = Path.Combine(hostEnvironment.ContentRootPath, "wwwroot")
});

builder.Logging.ClearProviders();

Expand Down Expand Up @@ -198,39 +205,70 @@ public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appMo

proxyBuilder.AddServiceDiscoveryDestinationResolver();

yarpResource.TryGetEndpoints(out var endpoints);
var defaultScheme = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Contains("https://") == true ? "https" : "http";
var needHttps = defaultScheme == "https" || endpoints?.Any(ep => ep.UriScheme == "https") == true;

if (needHttps)
{
builder.WebHost.UseKestrelHttpsConfiguration();
}

_app = builder.Build();

if (yarpResource.Endpoints.Count == 0)
var urlToEndpointNameMap = new Dictionary<string, string>();

if (endpoints is null)
{
_app.Urls.Add($"http://127.0.0.1:0");
var url = $"{defaultScheme}://127.0.0.1:0/";
_app.Urls.Add(url);
urlToEndpointNameMap[url] = "default";
}
else
{
foreach (var ep in yarpResource.Endpoints)
foreach (var ep in endpoints)
{
var scheme = ep.UriScheme ?? "http";
var scheme = ep.UriScheme ?? defaultScheme;
needHttps = needHttps || scheme == "https";

if (ep.Port is null)
{
_app.Urls.Add($"{scheme}://127.0.0.1:0");
}
else
var url = ep.Port switch
{
_app.Urls.Add($"{scheme}://localhost:{ep.Port}");
}
null => $"{scheme}://127.0.0.1:0/",
_ => $"{scheme}://localhost:{ep.Port}"
};

var uri = new Uri(url);
_app.Urls.Add(url);
urlToEndpointNameMap[uri.ToString()] = ep.Name;
}
}

_app.MapReverseProxy();

await _app.StartAsync(cancellationToken);

var urls = _app.Services.GetRequiredService<IServer>().Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;
var addresses = _app.Services.GetRequiredService<IServer>().Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;

// Update the EndpointAnnotations with the allocated URLs from ASP.NET Core
foreach (var url in addresses)
{
if (urlToEndpointNameMap.TryGetValue(new Uri(url).ToString(), out var name)
|| urlToEndpointNameMap.TryGetValue((new UriBuilder(url) { Port = 0 }).Uri.ToString(), out name))
{
var ep = endpoints?.FirstOrDefault(ep => ep.Name == name);
if (ep is not null)
{
var uri = new Uri(url);
var host = uri.Host is "127.0.0.1" or "[::1]" ? "localhost" : uri.Host;
ep.AllocatedEndpoint = new(ep, host, uri.Port);
}
}
}

await resourceNotificationService.PublishUpdateAsync(yarpResource, s => s with
{
State = "Running",
Urls = [.. urls.Select(u => new UrlSnapshot(u, u, IsInternal: false))]
Urls = [.. endpoints?.Select(ep => new UrlSnapshot(ep.Name, ep.AllocatedEndpoint?.UriString ?? "", IsInternal: false))],
});
}

Expand Down
16 changes: 14 additions & 2 deletions tests/SamplesTests/SamplesTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<Folder Include="Infrastructure\" />
<Compile Include="..\Shared\Infrastructure\**\*.cs" LinkBase="Infrastructure" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="GitHubActionsTestLogger" />
</ItemGroup>

<ItemGroup>
Expand Down
28 changes: 0 additions & 28 deletions tests/SamplesTests/YarpResource.cs

This file was deleted.

49 changes: 49 additions & 0 deletions tests/SamplesTests/YarpResourceSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Net;
using IntegrationTests.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit.Abstractions;

namespace SamplesTests.Tests;

public class YarpResourceSample(ITestOutputHelper testOutputHelper)
{
[Theory]
[InlineData(null)]
[InlineData(8001)]
public async Task GetAppsThroughYarpIngressResourceReturnsOkStatusCode(int? ingressPort)
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.YarpResource_AppHost>();
appHost.FixContentRoot();
appHost.WriteOutputTo(testOutputHelper);
if (ingressPort is not null)
{
appHost.Configuration.AddInMemoryCollection(new Dictionary<string, string?> { { "Ingress:Port", ingressPort.ToString() } });
}
appHost.Services.ConfigureHttpClientDefaults(options =>
{
options.AddStandardResilienceHandler(options =>
{
//options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(60);
//options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(360);
//options.CircuitBreaker.SamplingDuration = options.AttemptTimeout.Timeout * 2;
});
});

await using var app = await appHost.BuildAsync();
await app.StartAsync(waitForResourcesToStart: true);

// Act/Assert
var httpClient = app.CreateHttpClient("ingress");

var targets = new List<(string Path, string Name)> { ("/app1", "WebApplication1"), ("/app2", "WebApplication2") };
foreach (var target in targets)
{
var response = await httpClient.GetAsync(target.Path);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(target.Name, await response.Content.ReadAsStringAsync());
}
}
}
Loading