diff --git a/Aspire.sln b/Aspire.sln index be3505999d..a84a603ec1 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -21,8 +20,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogService", "playgroun EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasketService", "playground\TestShop\BasketService\BasketService.csproj", "{3FC74EA6-D554-4A87-AED5-A08FE407BBF4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "playground\TestShop\ApiGateway\ApiGateway.csproj", "{934625C7-243F-4659-9421-515301CF0263}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.StackExchange.Redis", "src\Components\Aspire.StackExchange.Redis\Aspire.StackExchange.Redis.csproj", "{7D4A7A84-B297-4777-9E2A-D8B1C427CAC7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{27381127-6C45-4B4C-8F18-41FF48DFE4B2}" @@ -661,6 +658,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Npgsql.EntityF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Components.Common.Tests", "tests\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj", "{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Yarp", "src\Aspire.Hosting.Yarp\Aspire.Hosting.Yarp.csproj", "{A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.AppService", "src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj", "{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}" @@ -741,18 +739,6 @@ Global {3FC74EA6-D554-4A87-AED5-A08FE407BBF4}.Release|x64.Build.0 = Release|Any CPU {3FC74EA6-D554-4A87-AED5-A08FE407BBF4}.Release|x86.ActiveCfg = Release|Any CPU {3FC74EA6-D554-4A87-AED5-A08FE407BBF4}.Release|x86.Build.0 = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|Any CPU.Build.0 = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|x64.ActiveCfg = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|x64.Build.0 = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|x86.ActiveCfg = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Debug|x86.Build.0 = Debug|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|Any CPU.ActiveCfg = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|Any CPU.Build.0 = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|x64.ActiveCfg = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|x64.Build.0 = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|x86.ActiveCfg = Release|Any CPU - {934625C7-243F-4659-9421-515301CF0263}.Release|x86.Build.0 = Release|Any CPU {7D4A7A84-B297-4777-9E2A-D8B1C427CAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D4A7A84-B297-4777-9E2A-D8B1C427CAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D4A7A84-B297-4777-9E2A-D8B1C427CAC7}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -3885,6 +3871,18 @@ Global {30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x64.Build.0 = Release|Any CPU {30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.ActiveCfg = Release|Any CPU {30950CEB-2232-F9FC-04FF-ADDCB8AC30A7}.Release|x86.Build.0 = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|x64.Build.0 = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Debug|x86.Build.0 = Debug|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|Any CPU.Build.0 = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x64.ActiveCfg = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x64.Build.0 = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x86.ActiveCfg = Release|Any CPU + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF}.Release|x86.Build.0 = Release|Any CPU {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -3943,7 +3941,6 @@ Global {C7B2309C-073A-4552-A508-A69768B64C6F} = {A68BA1A5-1604-433D-9778-DC0199831C2A} {6D04BB34-1CC6-4FF3-A02A-1FFAC2A7A4F3} = {A68BA1A5-1604-433D-9778-DC0199831C2A} {3FC74EA6-D554-4A87-AED5-A08FE407BBF4} = {A68BA1A5-1604-433D-9778-DC0199831C2A} - {934625C7-243F-4659-9421-515301CF0263} = {A68BA1A5-1604-433D-9778-DC0199831C2A} {7D4A7A84-B297-4777-9E2A-D8B1C427CAC7} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {145E569B-633F-4FF0-B273-CB1D3B49B9F2} = {63BB22D3-0FAC-4E87-BF9D-9FA2441684C9} {7570F683-EB48-467F-B1B1-70FF0153F52B} = {63BB22D3-0FAC-4E87-BF9D-9FA2441684C9} @@ -4250,6 +4247,7 @@ Global {192747A2-9338-DECF-5C8C-28EB8E13829B} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C} {30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C} + {A3399DE9-AAB0-43EA-B99B-6A62ABBDD7BF} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47} {6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {77CFE74A-32EE-400C-8930-5025E8555256} {5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {77CFE74A-32EE-400C-8930-5025E8555256} {2D9974C2-3AB2-FBFD-5156-080508BB7449} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index 9ebb61a89e..d28a70ba6a 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -77,7 +77,8 @@ builder.AddProject("orderprocessor", launchProfileName: "OrderProcessor") .WithReference(messaging).WaitFor(messaging); -builder.AddProject("apigateway") +builder.AddYarp("apigateway") + .WithConfigFile("yarp.json") .WithReference(basketService) .WithReference(catalogService); diff --git a/playground/TestShop/TestShop.AppHost/TestShop.AppHost.csproj b/playground/TestShop/TestShop.AppHost/TestShop.AppHost.csproj index 5868e33ca5..449ede7fa5 100644 --- a/playground/TestShop/TestShop.AppHost/TestShop.AppHost.csproj +++ b/playground/TestShop/TestShop.AppHost/TestShop.AppHost.csproj @@ -18,8 +18,7 @@ - - + diff --git a/playground/TestShop/TestShop.AppHost/appsettings.Development.json b/playground/TestShop/TestShop.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/playground/TestShop/TestShop.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/playground/TestShop/TestShop.AppHost/yarp.json b/playground/TestShop/TestShop.AppHost/yarp.json new file mode 100644 index 0000000000..475e17f852 --- /dev/null +++ b/playground/TestShop/TestShop.AppHost/yarp.json @@ -0,0 +1,49 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "catalog": { + "ClusterId": "catalog", + "Match": { + "Path": "/catalog/{**catch-all}" + }, + "Transforms": [ + { "PathRemovePrefix": "/catalog" } + ] + }, + "basket": { + "ClusterId": "basket", + "Match": { + "Path": "/basket/{**catch-all}" + }, + "Transforms": [ + { "PathRemovePrefix": "/basket" } + ] + } + }, + "Clusters": { + "catalog": { + "Destinations": { + "catalog": { + "Address": "http://catalogservice", + "Health": "http://catalogservice/readiness" + } + } + }, + "basket": { + "Destinations": { + "basket": { + "Address": "http://basketservice", + "Health": "http://basketservice/readiness" + } + } + } + } + } +} diff --git a/spelling.dic b/spelling.dic index 73800f6d3b..adac781f18 100644 --- a/spelling.dic +++ b/spelling.dic @@ -65,3 +65,4 @@ urls kubernetes Pgweb elasticsearch +Yarp diff --git a/src/Aspire.Hosting.Yarp/Aspire.Hosting.Yarp.csproj b/src/Aspire.Hosting.Yarp/Aspire.Hosting.Yarp.csproj new file mode 100644 index 0000000000..519f8af498 --- /dev/null +++ b/src/Aspire.Hosting.Yarp/Aspire.Hosting.Yarp.csproj @@ -0,0 +1,20 @@ + + + + $(DefaultTargetFramework) + true + aspire integration hosting yarp reverse-proxy + YARP support for .NET Aspire. + false + true + + + + 0 + + + + + + + diff --git a/src/Aspire.Hosting.Yarp/README.md b/src/Aspire.Hosting.Yarp/README.md new file mode 100644 index 0000000000..0352991b7e --- /dev/null +++ b/src/Aspire.Hosting.Yarp/README.md @@ -0,0 +1,92 @@ +# Aspire.Hosting.Yarp library + +Provides extension methods and resource definitions for a .NET Aspire AppHost to configure a YARP instance. + +## Getting started + +### Install the package + +In your AppHost project, install the .NET Aspire YARP Hosting library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Hosting.Yarp +``` + +## Usage example + +Then, in the _Program.cs_ file of `AppHost`, add a YARP resource and provide the configuration file using the following methods: + +```csharp +var catalogService = builder.AddProject("catalogservice") + [...]; +var basketService = builder.AddProject("basketservice") + [...]; + +builder.AddYarp("apigateway") + .WithConfigFile("yarp.json") + .WithReference(basketService) + .WithReference(catalogService); +``` + +The `yarp.json` configuration file can use the referenced service like this: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "AllowedHosts": "*", + "ReverseProxy": { + "Routes": { + "catalog": { + "ClusterId": "catalog", + "Match": { + "Path": "/catalog/{**catch-all}" + }, + "Transforms": [ + { "PathRemovePrefix": "/catalog" } + ] + }, + "basket": { + "ClusterId": "basket", + "Match": { + "Path": "/basket/{**catch-all}" + }, + "Transforms": [ + { "PathRemovePrefix": "/basket" } + ] + } + }, + "Clusters": { + "catalog": { + "Destinations": { + "catalog": { + "Address": "http://catalogservice", + } + } + }, + "basket": { + "Destinations": { + "basket": { + "Address": "http://basketservice", + } + } + } + } + } +} + +``` + +## Additional documentation + +* https://learn.microsoft.com/dotnet/aspire/caching/stackexchange-redis-component +* https://learn.microsoft.com/dotnet/aspire/caching/stackexchange-redis-output-caching-component +* https://learn.microsoft.com/dotnet/aspire/caching/stackexchange-redis-distributed-caching-component + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Aspire.Hosting.Yarp/YarpContainerImageTags.cs b/src/Aspire.Hosting.Yarp/YarpContainerImageTags.cs new file mode 100644 index 0000000000..cb28b279b1 --- /dev/null +++ b/src/Aspire.Hosting.Yarp/YarpContainerImageTags.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Yarp; + +internal static class YarpContainerImageTags +{ + public const string Registry = "mcr.microsoft.com"; + + public const string Image = "dotnet/nightly/yarp"; + + public const string Tag = "2-preview"; +} diff --git a/src/Aspire.Hosting.Yarp/YarpResource.cs b/src/Aspire.Hosting.Yarp/YarpResource.cs new file mode 100644 index 0000000000..0a19e15669 --- /dev/null +++ b/src/Aspire.Hosting.Yarp/YarpResource.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Yarp; + +/// +/// A resource that represents a YARP resource independent of the hosting model. +/// +/// The name of the resource. +public class YarpResource(string name) : ContainerResource(name) +{ + /// + /// File path of the config file for this YARP resource. + /// + internal string? ConfigFilePath { get; set; } +} diff --git a/src/Aspire.Hosting.Yarp/YarpServiceExtensions.cs b/src/Aspire.Hosting.Yarp/YarpServiceExtensions.cs new file mode 100644 index 0000000000..beb14d9af9 --- /dev/null +++ b/src/Aspire.Hosting.Yarp/YarpServiceExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Yarp; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding YARP resources to the application model. +/// +public static class YarpServiceExtensions +{ + private const int Port = 5000; + + private const string ConfigDirectory = "/etc"; + + private const string ConfigFileName = "yarp.config"; + + /// + /// Adds a YARP container to the application model. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// A reference to the . + public static IResourceBuilder AddYarp( + this IDistributedApplicationBuilder builder, + [ResourceName] string name) + { + var resource = new YarpResource(name); + + var yarpBuilder = builder.AddResource(resource) + .WithHttpEndpoint(targetPort: Port) + .WithImage(YarpContainerImageTags.Image) + .WithImageRegistry(YarpContainerImageTags.Registry) + .WithEnvironment("ASPNETCORE_ENVIRONMENT", builder.Environment.EnvironmentName) + .WithOtlpExporter(); + + if (builder.ExecutionContext.IsRunMode) + { + // YARP will not trust the cert used by Aspire otlp endpoint when running locally + // The Aspire otlp endpoint uses the dev cert, only valid for localhost, but from the container + // perspective, the url will be something like https://docker.host.internal, so it will NOT be valid. + yarpBuilder.WithEnvironment("YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE", "true"); + } + + // Map the configuration file + yarpBuilder.WithContainerFiles(ConfigDirectory, async (context, ct) => + { + string contents; + if (yarpBuilder.Resource.ConfigFilePath != null) + { + try + { + contents = await File.ReadAllTextAsync(yarpBuilder.Resource.ConfigFilePath, ct).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new DistributedApplicationException($"Error when reading the YARP config file '{yarpBuilder.Resource.ConfigFilePath}'", ex); + } + } + else + { + // TODO: build dynamically the config file if none provided. + throw new DistributedApplicationException($"No configuration provided for YARP instance \"{yarpBuilder.Resource.Name}\""); + } + + var configFile = new ContainerFile + { + Name = ConfigFileName, + Contents = contents + }; + + return [configFile]; + }); + + return yarpBuilder; + } + + /// + /// Set explicitly the config file to use for YARP. + /// + /// The YARP resource to configure. + /// The path to the YARP config file. + /// A reference to the . + public static IResourceBuilder WithConfigFile(this IResourceBuilder builder, string configFilePath) + { + builder.Resource.ConfigFilePath = configFilePath; + return builder; + } +} diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props index 09b26eff19..6a1d3ba77b 100644 --- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props +++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props @@ -56,6 +56,7 @@ +