diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b41c72..6055d7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,12 +29,37 @@ jobs: DOTNET_MULTILEVEL_LOOKUP: false DOTNET_INSTALL_DIR: ${{ matrix.os == 'ubuntu-latest' && '' || 'dotnet' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install .NET SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: global-json-file: ${{ matrix.os == 'ubuntu-latest' && './global.json' || '.\global.json' }} - name: Build - run: ${{ matrix.os == 'ubuntu-latest' && './build.sh' || '.\build.cmd' }} \ No newline at end of file + run: ${{ matrix.os == 'ubuntu-latest' && './build.sh' || '.\build.cmd' }} + + - name: Install .NET HTTPS Development Certificate + if: matrix.os == 'ubuntu-latest' + run: | + dotnet tool update -g linux-dev-certs + dotnet linux-dev-certs install + + - name: Test + id: test + # Can't run Docker on Windows agents yet + if: matrix.os == 'ubuntu-latest' + # Note that the space after the last double dash (--) is intentional + run: > + dotnet test ./eShop.sln + --logger console --logger trx --logger html --logger GitHubActions + --results-directory ./TestResults --blame + -- + RunConfiguration.CollectSourceInformation=true + + - name: Publish Test Results + if: (success() || steps.test.conclusion == 'failure') && matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: TestResults_${{ matrix.os }} + path: ./TestResults \ No newline at end of file diff --git a/README.md b/README.md index 0adbe52..cacc234 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ In this workshop, you'll learn by building out features of the [eShop Reference ### Using Windows and Visual Studio -If you're on Windows and using Visual Studio, you must use [Visual Studio 2022 Preview](https://visualstudio.com/preview) (version 17.9.0 Preview 5.0 or later). The preview version of Visual Studio 2022 is safe to install side-by-side with the release version. We recommend using Visual Studio 2022 Preview if you're on Windows as it includes support for working with .NET Aspire projects. +If you're on Windows and using Visual Studio, you must use [Visual Studio 2022 Preview](https://visualstudio.com/preview) (version 17.10.0 Preview 5.0 or later). The preview version of Visual Studio 2022 is safe to install side-by-side with the release version. We recommend using Visual Studio 2022 Preview if you're on Windows as it includes support for working with .NET Aspire projects. > Note: When installing Visual Studio you only need to install the `ASP.NET and web development` workload. @@ -20,14 +20,14 @@ If you're in an instructor-led workshop session and have issues downloading the ### Using macOS, Linux, or Windows but not using Visual Studio -If you're using macOs or Linux, or on Windows but don't want to use Visual Studio, you must [download](https://www.microsoft.com/net/download) and install the .NET SDK (version 8.0.101 or newer). You can use the editor or IDE of your choice but note that some operations might be more difficult due to lack of support for .NET Aspire at this time. +If you're using macOs or Linux, or on Windows but don't want to use Visual Studio, you must [download](https://www.microsoft.com/net/download) and install the .NET SDK (version 8.0.100 or newer). You can use the editor or IDE of your choice but note that some operations might be more difficult due to lack of support for .NET Aspire at this time. ### Updating and installing the .NET SDK workload for Aspire -After installing Visual Studio Preview or the required .NET SDK, you will need to update and install the .NET SDK workload for Aspire. This workshop is using an as yet unreleased preview of .NET Aspire (preview.3) which requires [special steps to install](https://github.com/dotnet/aspire/blob/dc8fa33195ef1f66b920206766b9224c4c3f19bd/docs/using-latest-daily.md#optional-using-scripts-to-install-the-latest-net-aspire-build-from-release-branches). For your convenience, scripts are provided in this repository to make this process easy: +After installing Visual Studio Preview or the required .NET SDK, you will need to update and install the .NET SDK workload for Aspire. This workshop is using the latest preview of .NET Aspire (preview.6). For your convenience, scripts are provided in this repository to make this process easy: 1. Clone [this repo](https://github.com/dotnet-presentations/eshop-app-workshop) to your machine. -1. In your terminal, navigate to the repo root and run the command `dotnet --version` to verify you are using version 8.0.101 or later of the .NET SDK: +1. In your terminal, navigate to the repo root and run the command `dotnet --version` to verify you are using version 8.0.100 or later of the .NET SDK: ```shell dotnet --version @@ -43,7 +43,7 @@ After installing Visual Studio Preview or the required .NET SDK, you will need t build.sh ``` -1. This script will download and install the latest build of the preview.3 version of the Aspire workload, followed by building all solutions in this repo. +1. This script will download and install the latest build of the preview.6 version of the Aspire workload, followed by building all solutions in this repo. 1. If your machine is successfully configured, you should see a message indicating the build succeeded: ```shell @@ -51,7 +51,6 @@ After installing Visual Studio Preview or the required .NET SDK, you will need t 0 Warning(s) 0 Error(s) ``` -1. For troubleshooting steps regarding installing and updating the Aspire workload, see the [documentation here](https://github.com/dotnet/aspire/blob/dc8fa33195ef1f66b920206766b9224c4c3f19bd/docs/using-latest-daily.md#troubleshooting). ## Labs diff --git a/build.cmd b/build.cmd index c841ca8..6cc6a9c 100644 --- a/build.cmd +++ b/build.cmd @@ -1,3 +1,5 @@ @ECHO OFF +dotnet workload install aspire + dotnet build .\build\Build.proj \ No newline at end of file diff --git a/build.sh b/build.sh index 4cea617..cd58c0d 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,5 @@ #!/usr/bin/env bash +dotnet workload install aspire + dotnet build ./build/Build.proj \ No newline at end of file diff --git a/eShop.sln b/eShop.sln index cb4fa90..73aa235 100644 --- a/eShop.sln +++ b/eShop.sln @@ -32,10 +32,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog.Data", "src\Catalog EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Catalog.Data.Manager", "src\Catalog.Data.Manager\Catalog.Data.Manager.csproj", "{E1AAD2C8-97A7-404E-9DF7-89A719101631}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ordering.Data", "src\Ordering.Data\Ordering.Data.csproj", "{D68FAD25-C09D-434C-A7AE-8F78DB2D923D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ordering.Data", "src\Ordering.Data\Ordering.Data.csproj", "{D68FAD25-C09D-434C-A7AE-8F78DB2D923D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ordering.Data.Manager", "src\Ordering.Data.Manager\Ordering.Data.Manager.csproj", "{4FCA8863-B3C7-47D6-B031-37A0384B40AA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "src\IntegrationTests\IntegrationTests.csproj", "{78A319E7-76E6-4813-9C36-CB90B406A91D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +84,10 @@ Global {4FCA8863-B3C7-47D6-B031-37A0384B40AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FCA8863-B3C7-47D6-B031-37A0384B40AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FCA8863-B3C7-47D6-B031-37A0384B40AA}.Release|Any CPU.Build.0 = Release|Any CPU + {78A319E7-76E6-4813-9C36-CB90B406A91D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78A319E7-76E6-4813-9C36-CB90B406A91D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78A319E7-76E6-4813-9C36-CB90B406A91D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78A319E7-76E6-4813-9C36-CB90B406A91D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/global.json b/global.json index 690654a..d6c191f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.100", "rollForward": "latestFeature", "allowPrerelease": true } diff --git a/kill b/kill deleted file mode 100644 index e69de29..0000000 diff --git a/labs/1-Create-Catalog-API/README.md b/labs/1-Create-Catalog-API/README.md index f34f236..78abdfb 100644 --- a/labs/1-Create-Catalog-API/README.md +++ b/labs/1-Create-Catalog-API/README.md @@ -132,6 +132,16 @@ Containers are extremely useful for hosting service dependencies, but rather tha ### Configuring PostgreSQL and pgAdmin +1. Install the `Aspire.Hosting.PostgreSQL` package in the `eShop.AppHost` project: + + ```shell + dotnet add package Aspire.Hosting.Redis + ``` + + ```xml + + ``` + 1. Use the methods on the `builder` variable to create a PostgreSQL instance called `postgres` with pgAdmin enabled, and a database called `CatalogDB`. Ensure that the `catalog-db-mgr` project resource is configured with a reference to the `catalogDb`: ```csharp diff --git a/labs/1-Create-Catalog-API/end/Catalog.API/Properties/launchSettings.json b/labs/1-Create-Catalog-API/end/Catalog.API/Properties/launchSettings.json index 6adf612..9b1c3b0 100644 --- a/labs/1-Create-Catalog-API/end/Catalog.API/Properties/launchSettings.json +++ b/labs/1-Create-Catalog-API/end/Catalog.API/Properties/launchSettings.json @@ -1,31 +1,21 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:63531", - "sslPort": 0 - } - }, "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5180", + "applicationUrl": "http://localhost:5222/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "IIS Express": { - "commandName": "IISExpress", + "https": { + "commandName": "Project", "launchBrowser": true, - "launchUrl": "swagger", + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/labs/1-Create-Catalog-API/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/1-Create-Catalog-API/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj index 66c18e9..764bd34 100644 --- a/labs/1-Create-Catalog-API/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/1-Create-Catalog-API/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/1-Create-Catalog-API/end/Catalog.Data/Catalog.Data.csproj b/labs/1-Create-Catalog-API/end/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/1-Create-Catalog-API/end/Catalog.Data/Catalog.Data.csproj +++ b/labs/1-Create-Catalog-API/end/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/1-Create-Catalog-API/end/eShop.AppHost/Properties/launchSettings.json b/labs/1-Create-Catalog-API/end/eShop.AppHost/Properties/launchSettings.json index 803f275..ae3ace6 100644 --- a/labs/1-Create-Catalog-API/end/eShop.AppHost/Properties/launchSettings.json +++ b/labs/1-Create-Catalog-API/end/eShop.AppHost/Properties/launchSettings.json @@ -1,15 +1,28 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17219;http://localhost:15178", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } + }, "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "applicationUrl": "http://localhost:15157", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16185" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" } } } diff --git a/labs/1-Create-Catalog-API/end/eShop.AppHost/eShop.AppHost.csproj b/labs/1-Create-Catalog-API/end/eShop.AppHost/eShop.AppHost.csproj index dcbbc70..e8a71f4 100644 --- a/labs/1-Create-Catalog-API/end/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/1-Create-Catalog-API/end/eShop.AppHost/eShop.AppHost.csproj @@ -9,7 +9,8 @@ - + + diff --git a/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/Extensions.cs b/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/Extensions.cs index 4921efe..6ee9ac6 100644 --- a/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/Extensions.cs +++ b/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/Extensions.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -33,26 +33,21 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) { - builder.Logging.AddOpenTelemetry(logging => + builder.Logging.AddOpenTelemetry(o => { - logging.IncludeFormattedMessage = true; - logging.IncludeScopes = true; + o.IncludeFormattedMessage = true; + o.IncludeScopes = true; }); builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - // We want to view all traces in development - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); @@ -69,19 +64,9 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => metrics.AddPrometheusExporter()); - - // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) - // builder.Services.AddOpenTelemetry() - // .UseAzureMonitor(); - return builder; } @@ -96,24 +81,20 @@ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicati public static WebApplication MapDefaultEndpoints(this WebApplication app) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); - - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index 3080588..0f94c4f 100644 --- a/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/1-Create-Catalog-API/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -12,13 +12,13 @@ - - - - - - - + + + + + + + diff --git a/labs/1-Create-Catalog-API/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/1-Create-Catalog-API/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index 0d55d31..f0e0b03 100644 --- a/labs/1-Create-Catalog-API/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/1-Create-Catalog-API/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/1-Create-Catalog-API/src/Catalog.Data/Catalog.Data.csproj b/labs/1-Create-Catalog-API/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/1-Create-Catalog-API/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/1-Create-Catalog-API/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/2-Create-Blazor-Frontend/README.md b/labs/2-Create-Blazor-Frontend/README.md index f9c577c..192193a 100644 --- a/labs/2-Create-Blazor-Frontend/README.md +++ b/labs/2-Create-Blazor-Frontend/README.md @@ -161,10 +161,10 @@ Now that we have a service we can use to easily retrieve the catalog items from ![Catalog page with no item images](./img/eshop-web-catalog-no-images.png) 1. Open the browser developer tools (F12) and navigate to the **Network** pane then refresh the page. From the failed requests log we can see the images are trying to be loaded from paths like `/product-images/99`, where `99` is the product ID but the site doesn't have these files or any endpoint configured to serve them. Like the product details, the product images are served by the Catalog API. -1. [YARP](https://microsoft.github.io/reverse-proxy/) is a package for ASP.NET Core applications that provides highly customizable reverse proxying capabilities. We'll use YARP to proxy the product image requests to the frontend site on to the Catalog API. Add a reference to the `Microsoft.Extensions.ServiceDiscovery.Yarp` package, version `8.0.0-preview.3.24105.21`. You can use the [`dotnet` CLI](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package), or Visual Studio NuGet Package Manager, or edit the `WebApp.csproj` directly: +1. [YARP](https://microsoft.github.io/reverse-proxy/) is a package for ASP.NET Core applications that provides highly customizable reverse proxying capabilities. We'll use YARP to proxy the product image requests to the frontend site on to the Catalog API. Add a reference to the `Microsoft.Extensions.ServiceDiscovery.Yarp` package, version `8.0.0-preview.6.24214.1`. You can use the [`dotnet` CLI](https://learn.microsoft.com/dotnet/core/tools/dotnet-add-package), or Visual Studio NuGet Package Manager, or edit the `WebApp.csproj` directly: ```xml - + ``` 1. To setup the proxying behavior, first add a line to the `AddApplicationServices` method in the `HostingExtensions.cs` file, to add the require services to the application's DI container: diff --git a/labs/2-Create-Blazor-Frontend/end/Catalog.API/Properties/launchSettings.json b/labs/2-Create-Blazor-Frontend/end/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/2-Create-Blazor-Frontend/end/Catalog.API/Properties/launchSettings.json +++ b/labs/2-Create-Blazor-Frontend/end/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/2-Create-Blazor-Frontend/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/2-Create-Blazor-Frontend/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/2-Create-Blazor-Frontend/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/2-Create-Blazor-Frontend/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/2-Create-Blazor-Frontend/end/Catalog.Data/Catalog.Data.csproj b/labs/2-Create-Blazor-Frontend/end/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/2-Create-Blazor-Frontend/end/Catalog.Data/Catalog.Data.csproj +++ b/labs/2-Create-Blazor-Frontend/end/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/2-Create-Blazor-Frontend/end/WebApp/WebApp.csproj b/labs/2-Create-Blazor-Frontend/end/WebApp/WebApp.csproj index 17b2494..8e297f0 100644 --- a/labs/2-Create-Blazor-Frontend/end/WebApp/WebApp.csproj +++ b/labs/2-Create-Blazor-Frontend/end/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/KeycloakResource.cs b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/KeycloakResource.cs index c6515c6..6ea30c8 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/KeycloakResource.cs +++ b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/KeycloakResource.cs @@ -17,7 +17,7 @@ public static IResourceBuilder AddKeycloakContainer( return builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") .WithArgs("start-dev") @@ -27,8 +27,8 @@ public static IResourceBuilder AddKeycloakContainer( public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { args.Clear(); args.Add("start-dev"); @@ -38,22 +38,22 @@ public static IResourceBuilder ImportRealms(this IRes return builder; } - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) + private static async Task WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) { var manifestResource = new KeycloakContainerResource(resource.Name); foreach (var annotation in resource.Annotations) { - if (annotation is not ExecutableArgsCallbackAnnotation) + if (annotation is not CommandLineArgsCallbackAnnotation) { manifestResource.Annotations.Add(annotation); } } // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); + manifestResource.Annotations.Add(new CommandLineArgsCallbackAnnotation(args => args.Add("start"))); - context.WriteContainer(resource); + await context.WriteContainerAsync(resource); } } diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Program.cs b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Program.cs index ef09035..0f81fcf 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Program.cs +++ b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Program.cs @@ -25,6 +25,6 @@ // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Properties/launchSettings.json b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Properties/launchSettings.json +++ b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/eShop.AppHost.csproj b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/eShop.AppHost.csproj index 383fa4c..12f452e 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/2-Create-Blazor-Frontend/end/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,8 @@ - + + diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/HostingExtensions.cs b/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index 896d3cc..000a26e 100644 --- a/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/2-Create-Blazor-Frontend/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -15,13 +15,13 @@ - + - - - - - - + + + + + + diff --git a/labs/2-Create-Blazor-Frontend/src/Catalog.API/Properties/launchSettings.json b/labs/2-Create-Blazor-Frontend/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/2-Create-Blazor-Frontend/src/Catalog.API/Properties/launchSettings.json +++ b/labs/2-Create-Blazor-Frontend/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/2-Create-Blazor-Frontend/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/2-Create-Blazor-Frontend/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/2-Create-Blazor-Frontend/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/2-Create-Blazor-Frontend/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/2-Create-Blazor-Frontend/src/Catalog.Data/Catalog.Data.csproj b/labs/2-Create-Blazor-Frontend/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/2-Create-Blazor-Frontend/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/2-Create-Blazor-Frontend/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/KeycloakResource.cs b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/KeycloakResource.cs index c6515c6..6ea30c8 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/KeycloakResource.cs +++ b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/KeycloakResource.cs @@ -17,7 +17,7 @@ public static IResourceBuilder AddKeycloakContainer( return builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") .WithArgs("start-dev") @@ -27,8 +27,8 @@ public static IResourceBuilder AddKeycloakContainer( public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { args.Clear(); args.Add("start-dev"); @@ -38,22 +38,22 @@ public static IResourceBuilder ImportRealms(this IRes return builder; } - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) + private static async Task WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) { var manifestResource = new KeycloakContainerResource(resource.Name); foreach (var annotation in resource.Annotations) { - if (annotation is not ExecutableArgsCallbackAnnotation) + if (annotation is not CommandLineArgsCallbackAnnotation) { manifestResource.Annotations.Add(annotation); } } // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); + manifestResource.Annotations.Add(new CommandLineArgsCallbackAnnotation(args => args.Add("start"))); - context.WriteContainer(resource); + await context.WriteContainerAsync(resource); } } diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Program.cs b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Program.cs index 49ad41c..4839d06 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Program.cs +++ b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Program.cs @@ -23,6 +23,6 @@ // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Properties/launchSettings.json b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Properties/launchSettings.json +++ b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/eShop.AppHost.csproj b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/eShop.AppHost.csproj index dc26e8a..f785ca0 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/2-Create-Blazor-Frontend/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,8 @@ - + + diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/HostingExtensions.cs b/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index 896d3cc..000a26e 100644 --- a/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/2-Create-Blazor-Frontend/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -15,13 +15,13 @@ - + - - - - - - + + + + + + diff --git a/labs/3-Add-Identity/README.md b/labs/3-Add-Identity/README.md index 2b5f5a4..c76ebce 100644 --- a/labs/3-Add-Identity/README.md +++ b/labs/3-Add-Identity/README.md @@ -41,8 +41,8 @@ You can read more about [selecting an identity management solution for ASP.NET C ```csharp // Inject the project URLs for Keycloak realm configuration - idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); - idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); + idp.WithEnvironment("WEBAPP_HTTP", webApp.GetEndpoint("http")); + idp.WithEnvironment("WEBAPP_HTTPS", webApp.GetEndpoint("https")); ``` 1. Run the AppHost project again and verify that the container starts successfully. This can be confirmed by finding the following lines in the container's logs: @@ -75,11 +75,10 @@ You can read more about [selecting an identity management solution for ASP.NET C 1. Now that we've confirmed that our Keycloak instance is successfully configured, update the `Program.cs` file of the AppHost project so that the `webapp` resource references the `idp` Keycloak resource, using the `WithReference` method. This will ensure that the `webapp` resource will have configuration values injected via its environment variables so that it can resovle calls to `http://idp` with the actual address assigned when the project is launched. Additionally, use the `WithLaunchProfile` method to ensure the `webapp` resource is always launched using the `"https"` launch profile (defined in its `Properties/launchSettings.json` file) as OIDC-based authentication flows typically require HTTPS to be used: ```csharp - var webApp = builder.AddProject("webapp") + // Force HTTPS profile for web app (required for OIDC operations) + var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(catalogApi) .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); ``` 1. Launch the AppHost project again and use the dashboard to verify that the address of the `idp` resource was injected into the `webapp` resource via environment variables: diff --git a/labs/3-Add-Identity/end/Catalog.API/Properties/launchSettings.json b/labs/3-Add-Identity/end/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/3-Add-Identity/end/Catalog.API/Properties/launchSettings.json +++ b/labs/3-Add-Identity/end/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/3-Add-Identity/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/3-Add-Identity/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/3-Add-Identity/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/3-Add-Identity/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/3-Add-Identity/end/Catalog.Data/Catalog.Data.csproj b/labs/3-Add-Identity/end/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/3-Add-Identity/end/Catalog.Data/Catalog.Data.csproj +++ b/labs/3-Add-Identity/end/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/3-Add-Identity/end/WebApp/WebApp.csproj b/labs/3-Add-Identity/end/WebApp/WebApp.csproj index 19ee27b..79e6a11 100644 --- a/labs/3-Add-Identity/end/WebApp/WebApp.csproj +++ b/labs/3-Add-Identity/end/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/3-Add-Identity/end/eShop.AppHost/KeycloakResource.cs b/labs/3-Add-Identity/end/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/3-Add-Identity/end/eShop.AppHost/KeycloakResource.cs +++ b/labs/3-Add-Identity/end/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/3-Add-Identity/end/eShop.AppHost/Program.cs b/labs/3-Add-Identity/end/eShop.AppHost/Program.cs index 597fa1a..713e092 100644 --- a/labs/3-Add-Identity/end/eShop.AppHost/Program.cs +++ b/labs/3-Add-Identity/end/eShop.AppHost/Program.cs @@ -25,18 +25,20 @@ // Apps - -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(catalogApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/3-Add-Identity/end/eShop.AppHost/Properties/launchSettings.json b/labs/3-Add-Identity/end/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/3-Add-Identity/end/eShop.AppHost/Properties/launchSettings.json +++ b/labs/3-Add-Identity/end/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/3-Add-Identity/end/eShop.AppHost/eShop.AppHost.csproj b/labs/3-Add-Identity/end/eShop.AppHost/eShop.AppHost.csproj index 383fa4c..12f452e 100644 --- a/labs/3-Add-Identity/end/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/3-Add-Identity/end/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,8 @@ - + + diff --git a/labs/3-Add-Identity/end/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/3-Add-Identity/end/eShop.ServiceDefaults/AuthenticationExtensions.cs index a5bdab0..84c3c3d 100644 --- a/labs/3-Add-Identity/end/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/3-Add-Identity/end/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -40,7 +40,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/3-Add-Identity/end/eShop.ServiceDefaults/HostingExtensions.cs b/labs/3-Add-Identity/end/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/3-Add-Identity/end/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/3-Add-Identity/end/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/3-Add-Identity/end/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/3-Add-Identity/end/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/3-Add-Identity/end/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/3-Add-Identity/end/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/3-Add-Identity/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/3-Add-Identity/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/3-Add-Identity/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/3-Add-Identity/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/3-Add-Identity/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/3-Add-Identity/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/3-Add-Identity/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/3-Add-Identity/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/3-Add-Identity/src/Catalog.API/Properties/launchSettings.json b/labs/3-Add-Identity/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/3-Add-Identity/src/Catalog.API/Properties/launchSettings.json +++ b/labs/3-Add-Identity/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/3-Add-Identity/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/3-Add-Identity/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/3-Add-Identity/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/3-Add-Identity/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/3-Add-Identity/src/Catalog.Data/Catalog.Data.csproj b/labs/3-Add-Identity/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/3-Add-Identity/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/3-Add-Identity/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/3-Add-Identity/src/Keycloak/data/import/eshop-realm.json b/labs/3-Add-Identity/src/Keycloak/data/import/eshop-realm.json index 4b4ff55..68ecb7e 100644 --- a/labs/3-Add-Identity/src/Keycloak/data/import/eshop-realm.json +++ b/labs/3-Add-Identity/src/Keycloak/data/import/eshop-realm.json @@ -255,6 +255,7 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "orderingswaggerui" : [ ], "admin-cli" : [ ], "account-console" : [ ], "broker" : [ { @@ -560,6 +561,42 @@ "nodeReRegistrationTimeout" : 0, "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c", + "clientId" : "orderingswaggerui", + "name" : "Ordering Swagger UI", + "description" : "", + "rootUrl" : "${ORDERINGAPI_HTTP}", + "adminUrl" : "${ORDERINGAPI_HTTP}", + "baseUrl" : "${ORDERINGAPI_HTTP}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "${ORDERINGAPI_HTTP}/*" ], + "webOrigins" : [ "${ORDERINGAPI_HTTP}" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : true, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0", "clientId" : "realm-management", @@ -640,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/3-Add-Identity/src/WebApp/WebApp.csproj b/labs/3-Add-Identity/src/WebApp/WebApp.csproj index 54c9da7..bd26471 100644 --- a/labs/3-Add-Identity/src/WebApp/WebApp.csproj +++ b/labs/3-Add-Identity/src/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/3-Add-Identity/src/eShop.AppHost/KeycloakResource.cs b/labs/3-Add-Identity/src/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/3-Add-Identity/src/eShop.AppHost/KeycloakResource.cs +++ b/labs/3-Add-Identity/src/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/3-Add-Identity/src/eShop.AppHost/Program.cs b/labs/3-Add-Identity/src/eShop.AppHost/Program.cs index ee6108d..6574b84 100644 --- a/labs/3-Add-Identity/src/eShop.AppHost/Program.cs +++ b/labs/3-Add-Identity/src/eShop.AppHost/Program.cs @@ -25,6 +25,6 @@ .WithReference(catalogApi); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/3-Add-Identity/src/eShop.AppHost/Properties/launchSettings.json b/labs/3-Add-Identity/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/3-Add-Identity/src/eShop.AppHost/Properties/launchSettings.json +++ b/labs/3-Add-Identity/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/3-Add-Identity/src/eShop.AppHost/eShop.AppHost.csproj b/labs/3-Add-Identity/src/eShop.AppHost/eShop.AppHost.csproj index 383fa4c..12f452e 100644 --- a/labs/3-Add-Identity/src/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/3-Add-Identity/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,8 @@ - + + diff --git a/labs/3-Add-Identity/src/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/3-Add-Identity/src/eShop.ServiceDefaults/AuthenticationExtensions.cs index a5bdab0..84c3c3d 100644 --- a/labs/3-Add-Identity/src/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/3-Add-Identity/src/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -40,7 +40,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/3-Add-Identity/src/eShop.ServiceDefaults/HostingExtensions.cs b/labs/3-Add-Identity/src/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/3-Add-Identity/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/3-Add-Identity/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/3-Add-Identity/src/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/3-Add-Identity/src/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/3-Add-Identity/src/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/3-Add-Identity/src/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/3-Add-Identity/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/3-Add-Identity/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/3-Add-Identity/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/3-Add-Identity/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/3-Add-Identity/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/3-Add-Identity/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/3-Add-Identity/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/3-Add-Identity/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/4-Add-Shopping-Basket/README.md b/labs/4-Add-Shopping-Basket/README.md index 2771c40..d8af7fc 100644 --- a/labs/4-Add-Shopping-Basket/README.md +++ b/labs/4-Add-Shopping-Basket/README.md @@ -20,6 +20,16 @@ In previous labs, we have created a web site that shoppers can use to browser a eShop.Basket.API ``` +1. Install the `Aspire.Hosting.Redis` package in the `eShop.AppHost` project: + + ```shell + dotnet add package Aspire.Hosting.Redis + ``` + + ```xml + + ``` + 1. Open the `Program.cs` file in the `eShop.AppHost` project and add a line to create a new Redis resource named `"BasketStore"` and configure it to host a [Redis Commander](https://joeferner.github.io/redis-commander/) instance too (this will make it easier to inspect the Redis database during development). Capture the resource in a `basketStore` variable: ```csharp @@ -41,12 +51,11 @@ In previous labs, we have created a web site that shoppers can use to browser a 1. Update the `webapp` resource to reference the `basket-api` resource so the web site can communicate with the Basket API: ```csharp - var webApp = builder.AddProject("webapp") + // Force HTTPS profile for web app (required for OIDC operations) + var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(catalogApi) .WithReference(basketApi) // <--- Add this line - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); ``` 1. Run the AppHost project and verify that the containers for Redis and Redis Commander are created and running by using the dashboard. Also verify that the `Basket.API` project is running and that it's environment variables contain the configuration values to communicate with the IdP and Redis. @@ -83,7 +92,7 @@ In previous labs, we have created a web site that shoppers can use to browser a Add the `Aspire.StackExchange.Redis` component NuGet package to the `Basket.API` project. You can use the **Add > .NET Aspire Compoenent...** project menu item in Visual Studio, the `dotnet add package` command at the command line, or by editing the `Basket.API.csproj` file directly: ```xml - + ``` 1. In the `AddApplicationServices` method in `HostingExtensions.cs`, add a call to `AddRedis` to configure the Redis client in the application's DI container. Pass the name `"BasketStore"` to the method to indicate that the client should be configured to connect to the Redis resource with that name in the AppHost: @@ -91,7 +100,7 @@ In previous labs, we have created a web site that shoppers can use to browser a ```csharp public static IHostApplicationBuilder AddApplicationServices(this IHostApplicationBuilder builder) { - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); return builder; } @@ -372,7 +381,7 @@ In previous labs, we have created a web site that shoppers can use to browser a { builder.AddDefaultAuthentication(); // <-- Add this line - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); diff --git a/labs/4-Add-Shopping-Basket/end/Basket.API/Basket.API.csproj b/labs/4-Add-Shopping-Basket/end/Basket.API/Basket.API.csproj index 8e2155f..d036c99 100644 --- a/labs/4-Add-Shopping-Basket/end/Basket.API/Basket.API.csproj +++ b/labs/4-Add-Shopping-Basket/end/Basket.API/Basket.API.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/4-Add-Shopping-Basket/end/Basket.API/Extensions/HostingExtensions.cs b/labs/4-Add-Shopping-Basket/end/Basket.API/Extensions/HostingExtensions.cs index 6674219..4d7b24b 100644 --- a/labs/4-Add-Shopping-Basket/end/Basket.API/Extensions/HostingExtensions.cs +++ b/labs/4-Add-Shopping-Basket/end/Basket.API/Extensions/HostingExtensions.cs @@ -8,7 +8,7 @@ public static IHostApplicationBuilder AddApplicationServices(this IHostApplicati { builder.AddDefaultAuthentication(); - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); diff --git a/labs/4-Add-Shopping-Basket/end/Catalog.API/Properties/launchSettings.json b/labs/4-Add-Shopping-Basket/end/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/4-Add-Shopping-Basket/end/Catalog.API/Properties/launchSettings.json +++ b/labs/4-Add-Shopping-Basket/end/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/4-Add-Shopping-Basket/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/4-Add-Shopping-Basket/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/4-Add-Shopping-Basket/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/4-Add-Shopping-Basket/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/4-Add-Shopping-Basket/end/Catalog.Data/Catalog.Data.csproj b/labs/4-Add-Shopping-Basket/end/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/4-Add-Shopping-Basket/end/Catalog.Data/Catalog.Data.csproj +++ b/labs/4-Add-Shopping-Basket/end/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/4-Add-Shopping-Basket/end/Keycloak/data/import/eshop-realm.json b/labs/4-Add-Shopping-Basket/end/Keycloak/data/import/eshop-realm.json index 4b4ff55..68ecb7e 100644 --- a/labs/4-Add-Shopping-Basket/end/Keycloak/data/import/eshop-realm.json +++ b/labs/4-Add-Shopping-Basket/end/Keycloak/data/import/eshop-realm.json @@ -255,6 +255,7 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "orderingswaggerui" : [ ], "admin-cli" : [ ], "account-console" : [ ], "broker" : [ { @@ -560,6 +561,42 @@ "nodeReRegistrationTimeout" : 0, "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c", + "clientId" : "orderingswaggerui", + "name" : "Ordering Swagger UI", + "description" : "", + "rootUrl" : "${ORDERINGAPI_HTTP}", + "adminUrl" : "${ORDERINGAPI_HTTP}", + "baseUrl" : "${ORDERINGAPI_HTTP}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "${ORDERINGAPI_HTTP}/*" ], + "webOrigins" : [ "${ORDERINGAPI_HTTP}" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : true, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0", "clientId" : "realm-management", @@ -640,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/4-Add-Shopping-Basket/end/WebApp/WebApp.csproj b/labs/4-Add-Shopping-Basket/end/WebApp/WebApp.csproj index 06b8257..84bee86 100644 --- a/labs/4-Add-Shopping-Basket/end/WebApp/WebApp.csproj +++ b/labs/4-Add-Shopping-Basket/end/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/KeycloakResource.cs b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/KeycloakResource.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Program.cs b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Program.cs index c55b3ad..4e0016b 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Program.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Program.cs @@ -27,18 +27,21 @@ // Apps -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(catalogApi) .WithReference(basketApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Properties/launchSettings.json b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Properties/launchSettings.json +++ b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/eShop.AppHost.csproj b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/eShop.AppHost.csproj index dc6df60..a83e7dd 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/4-Add-Shopping-Basket/end/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/AuthenticationExtensions.cs index a5bdab0..84c3c3d 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -40,7 +40,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/HostingExtensions.cs b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/4-Add-Shopping-Basket/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/4-Add-Shopping-Basket/src/Catalog.API/Properties/launchSettings.json b/labs/4-Add-Shopping-Basket/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/4-Add-Shopping-Basket/src/Catalog.API/Properties/launchSettings.json +++ b/labs/4-Add-Shopping-Basket/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/4-Add-Shopping-Basket/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/4-Add-Shopping-Basket/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/4-Add-Shopping-Basket/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/4-Add-Shopping-Basket/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/4-Add-Shopping-Basket/src/Catalog.Data/Catalog.Data.csproj b/labs/4-Add-Shopping-Basket/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/4-Add-Shopping-Basket/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/4-Add-Shopping-Basket/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/4-Add-Shopping-Basket/src/Keycloak/data/import/eshop-realm.json b/labs/4-Add-Shopping-Basket/src/Keycloak/data/import/eshop-realm.json index 4b4ff55..68ecb7e 100644 --- a/labs/4-Add-Shopping-Basket/src/Keycloak/data/import/eshop-realm.json +++ b/labs/4-Add-Shopping-Basket/src/Keycloak/data/import/eshop-realm.json @@ -255,6 +255,7 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "orderingswaggerui" : [ ], "admin-cli" : [ ], "account-console" : [ ], "broker" : [ { @@ -560,6 +561,42 @@ "nodeReRegistrationTimeout" : 0, "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c", + "clientId" : "orderingswaggerui", + "name" : "Ordering Swagger UI", + "description" : "", + "rootUrl" : "${ORDERINGAPI_HTTP}", + "adminUrl" : "${ORDERINGAPI_HTTP}", + "baseUrl" : "${ORDERINGAPI_HTTP}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "${ORDERINGAPI_HTTP}/*" ], + "webOrigins" : [ "${ORDERINGAPI_HTTP}" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : true, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] }, { "id" : "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0", "clientId" : "realm-management", @@ -640,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/4-Add-Shopping-Basket/src/WebApp/WebApp.csproj b/labs/4-Add-Shopping-Basket/src/WebApp/WebApp.csproj index 19ee27b..79e6a11 100644 --- a/labs/4-Add-Shopping-Basket/src/WebApp/WebApp.csproj +++ b/labs/4-Add-Shopping-Basket/src/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/KeycloakResource.cs b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/KeycloakResource.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Program.cs b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Program.cs index 14c6e9a..f05ab87 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Program.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Program.cs @@ -24,17 +24,20 @@ // Apps -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(catalogApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Properties/launchSettings.json b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Properties/launchSettings.json +++ b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/eShop.AppHost.csproj b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/eShop.AppHost.csproj index 383fa4c..12f452e 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/4-Add-Shopping-Basket/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,8 @@ - + + diff --git a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/AuthenticationExtensions.cs index a5bdab0..84c3c3d 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -40,7 +40,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/HostingExtensions.cs b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/4-Add-Shopping-Basket/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/5-Add-Checkout/README.md b/labs/5-Add-Checkout/README.md index 9f143ea..543bafc 100644 --- a/labs/5-Add-Checkout/README.md +++ b/labs/5-Add-Checkout/README.md @@ -41,7 +41,7 @@ Now that we've verified the Ordering database is working, let's add an HTTP API 1. Towards the bottom of the `Program.cs` file, udpate the line that adds the `"ORDERINGAPI_HTTP"` envionment variable to the `idp` resource so that it now passes in the `http` endpoint from the `orderingApi` resource. This will ensure the IdP is configured correctly to support authentication requests from the `Ordering.API` project: ```csharp - idp.WithEnvironment("ORDERINGAPI_HTTP", () => orderingApi.GetEndpoint("http").UriString); + idp.WithEnvironment("ORDERINGAPI_HTTP", orderingApi.GetEndpoint("http")); ``` 1. Add a project reference from the `Ordering.API` project to the `Ordering.Data` project so that it can use Entity Framework Core to access the database. diff --git a/labs/5-Add-Checkout/end/Basket.API/Basket.API.csproj b/labs/5-Add-Checkout/end/Basket.API/Basket.API.csproj index 9b2eeb2..8f95a9b 100644 --- a/labs/5-Add-Checkout/end/Basket.API/Basket.API.csproj +++ b/labs/5-Add-Checkout/end/Basket.API/Basket.API.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/5-Add-Checkout/end/Basket.API/Extensions/HostingExtensions.cs b/labs/5-Add-Checkout/end/Basket.API/Extensions/HostingExtensions.cs index 6674219..4d7b24b 100644 --- a/labs/5-Add-Checkout/end/Basket.API/Extensions/HostingExtensions.cs +++ b/labs/5-Add-Checkout/end/Basket.API/Extensions/HostingExtensions.cs @@ -8,7 +8,7 @@ public static IHostApplicationBuilder AddApplicationServices(this IHostApplicati { builder.AddDefaultAuthentication(); - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); diff --git a/labs/5-Add-Checkout/end/Catalog.API/Properties/launchSettings.json b/labs/5-Add-Checkout/end/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/5-Add-Checkout/end/Catalog.API/Properties/launchSettings.json +++ b/labs/5-Add-Checkout/end/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/5-Add-Checkout/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/5-Add-Checkout/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/5-Add-Checkout/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/5-Add-Checkout/end/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/5-Add-Checkout/end/Catalog.Data/Catalog.Data.csproj b/labs/5-Add-Checkout/end/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/5-Add-Checkout/end/Catalog.Data/Catalog.Data.csproj +++ b/labs/5-Add-Checkout/end/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/5-Add-Checkout/end/Keycloak/data/import/eshop-realm.json b/labs/5-Add-Checkout/end/Keycloak/data/import/eshop-realm.json index 6361cbc..68ecb7e 100644 --- a/labs/5-Add-Checkout/end/Keycloak/data/import/eshop-realm.json +++ b/labs/5-Add-Checkout/end/Keycloak/data/import/eshop-realm.json @@ -677,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/5-Add-Checkout/end/Ordering.API/Properties/launchSettings.json b/labs/5-Add-Checkout/end/Ordering.API/Properties/launchSettings.json index d12198d..110b7ff 100644 --- a/labs/5-Add-Checkout/end/Ordering.API/Properties/launchSettings.json +++ b/labs/5-Add-Checkout/end/Ordering.API/Properties/launchSettings.json @@ -3,13 +3,19 @@ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5122", + "applicationUrl": "http://localhost:5224", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7360;http://localhost:5224", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/labs/5-Add-Checkout/end/Ordering.Data.Manager/Ordering.Data.Manager.csproj b/labs/5-Add-Checkout/end/Ordering.Data.Manager/Ordering.Data.Manager.csproj index 7fc2650..fbdd23a 100644 --- a/labs/5-Add-Checkout/end/Ordering.Data.Manager/Ordering.Data.Manager.csproj +++ b/labs/5-Add-Checkout/end/Ordering.Data.Manager/Ordering.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/5-Add-Checkout/end/Ordering.Data/Ordering.Data.csproj b/labs/5-Add-Checkout/end/Ordering.Data/Ordering.Data.csproj index 087850e..9568419 100644 --- a/labs/5-Add-Checkout/end/Ordering.Data/Ordering.Data.csproj +++ b/labs/5-Add-Checkout/end/Ordering.Data/Ordering.Data.csproj @@ -8,6 +8,6 @@ - + diff --git a/labs/5-Add-Checkout/end/WebApp/WebApp.csproj b/labs/5-Add-Checkout/end/WebApp/WebApp.csproj index a260e77..2b8bb1a 100644 --- a/labs/5-Add-Checkout/end/WebApp/WebApp.csproj +++ b/labs/5-Add-Checkout/end/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/5-Add-Checkout/end/eShop.AppHost/KeycloakResource.cs b/labs/5-Add-Checkout/end/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/5-Add-Checkout/end/eShop.AppHost/KeycloakResource.cs +++ b/labs/5-Add-Checkout/end/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/5-Add-Checkout/end/eShop.AppHost/Program.cs b/labs/5-Add-Checkout/end/eShop.AppHost/Program.cs index 440486d..93663df 100644 --- a/labs/5-Add-Checkout/end/eShop.AppHost/Program.cs +++ b/labs/5-Add-Checkout/end/eShop.AppHost/Program.cs @@ -37,19 +37,22 @@ // Apps -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(basketApi) .WithReference(catalogApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); -idp.WithEnvironment("ORDERINGAPI_HTTP", () => orderingApi.GetEndpoint("http").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); +idp.WithEnvironment("ORDERINGAPI_HTTP", orderingApi.GetEndpoint("http")); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/5-Add-Checkout/end/eShop.AppHost/Properties/launchSettings.json b/labs/5-Add-Checkout/end/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/5-Add-Checkout/end/eShop.AppHost/Properties/launchSettings.json +++ b/labs/5-Add-Checkout/end/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/5-Add-Checkout/end/eShop.AppHost/eShop.AppHost.csproj b/labs/5-Add-Checkout/end/eShop.AppHost/eShop.AppHost.csproj index e0bb1c6..d47288f 100644 --- a/labs/5-Add-Checkout/end/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/5-Add-Checkout/end/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/AuthenticationExtensions.cs index ac0cd81..9e183ad 100644 --- a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -44,7 +44,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/HostingExtensions.cs b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/5-Add-Checkout/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/5-Add-Checkout/end/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/5-Add-Checkout/src/Basket.API/Basket.API.csproj b/labs/5-Add-Checkout/src/Basket.API/Basket.API.csproj index 9b2eeb2..8f95a9b 100644 --- a/labs/5-Add-Checkout/src/Basket.API/Basket.API.csproj +++ b/labs/5-Add-Checkout/src/Basket.API/Basket.API.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/5-Add-Checkout/src/Basket.API/Extensions/HostingExtensions.cs b/labs/5-Add-Checkout/src/Basket.API/Extensions/HostingExtensions.cs index 6674219..4d7b24b 100644 --- a/labs/5-Add-Checkout/src/Basket.API/Extensions/HostingExtensions.cs +++ b/labs/5-Add-Checkout/src/Basket.API/Extensions/HostingExtensions.cs @@ -8,7 +8,7 @@ public static IHostApplicationBuilder AddApplicationServices(this IHostApplicati { builder.AddDefaultAuthentication(); - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); diff --git a/labs/5-Add-Checkout/src/Catalog.API/Properties/launchSettings.json b/labs/5-Add-Checkout/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/5-Add-Checkout/src/Catalog.API/Properties/launchSettings.json +++ b/labs/5-Add-Checkout/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/5-Add-Checkout/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/5-Add-Checkout/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/5-Add-Checkout/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/5-Add-Checkout/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/5-Add-Checkout/src/Catalog.Data/Catalog.Data.csproj b/labs/5-Add-Checkout/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/5-Add-Checkout/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/5-Add-Checkout/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/5-Add-Checkout/src/Keycloak/data/import/eshop-realm.json b/labs/5-Add-Checkout/src/Keycloak/data/import/eshop-realm.json index 6361cbc..68ecb7e 100644 --- a/labs/5-Add-Checkout/src/Keycloak/data/import/eshop-realm.json +++ b/labs/5-Add-Checkout/src/Keycloak/data/import/eshop-realm.json @@ -677,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/5-Add-Checkout/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj b/labs/5-Add-Checkout/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj index 7fc2650..fbdd23a 100644 --- a/labs/5-Add-Checkout/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj +++ b/labs/5-Add-Checkout/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/5-Add-Checkout/src/Ordering.Data/Ordering.Data.csproj b/labs/5-Add-Checkout/src/Ordering.Data/Ordering.Data.csproj index 087850e..9568419 100644 --- a/labs/5-Add-Checkout/src/Ordering.Data/Ordering.Data.csproj +++ b/labs/5-Add-Checkout/src/Ordering.Data/Ordering.Data.csproj @@ -8,6 +8,6 @@ - + diff --git a/labs/5-Add-Checkout/src/WebApp/WebApp.csproj b/labs/5-Add-Checkout/src/WebApp/WebApp.csproj index a260e77..2b8bb1a 100644 --- a/labs/5-Add-Checkout/src/WebApp/WebApp.csproj +++ b/labs/5-Add-Checkout/src/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/5-Add-Checkout/src/eShop.AppHost/KeycloakResource.cs b/labs/5-Add-Checkout/src/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/5-Add-Checkout/src/eShop.AppHost/KeycloakResource.cs +++ b/labs/5-Add-Checkout/src/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/5-Add-Checkout/src/eShop.AppHost/Program.cs b/labs/5-Add-Checkout/src/eShop.AppHost/Program.cs index 0fd0b3d..4fccff4 100644 --- a/labs/5-Add-Checkout/src/eShop.AppHost/Program.cs +++ b/labs/5-Add-Checkout/src/eShop.AppHost/Program.cs @@ -33,19 +33,22 @@ // Apps -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(basketApi) .WithReference(catalogApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); idp.WithEnvironment("ORDERINGAPI_HTTP", () => "http://placeholder-for-ordering-api"); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/5-Add-Checkout/src/eShop.AppHost/Properties/launchSettings.json b/labs/5-Add-Checkout/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/5-Add-Checkout/src/eShop.AppHost/Properties/launchSettings.json +++ b/labs/5-Add-Checkout/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/5-Add-Checkout/src/eShop.AppHost/eShop.AppHost.csproj b/labs/5-Add-Checkout/src/eShop.AppHost/eShop.AppHost.csproj index 206c390..d0b1bc6 100644 --- a/labs/5-Add-Checkout/src/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/5-Add-Checkout/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/AuthenticationExtensions.cs index ac0cd81..9e183ad 100644 --- a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -44,7 +44,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/HostingExtensions.cs b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/HostingExtensions.cs index 2cd88e4..062c139 100644 --- a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -25,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -42,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -67,51 +65,57 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } - public static WebApplication MapDefaultEndpoints(this WebApplication app) + public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); + // The developer exception page is used automatically in development + if (!app.Environment.IsDevelopment()) + { + if (errorHandlingPath is not null) + { + app.UseExceptionHandler(errorHandlingPath); + } + else if (app.Services.GetService() is not null) + { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. + app.UseExceptionHandler(); + } + } - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); + return app; + } - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/OpenApiExtensions.cs index 6fe5d25..08df631 100644 --- a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, httpClientFactory, serviceEndPointResolver) => + .Configure((options, httpClientFactory, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/5-Add-Checkout/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/5-Add-Checkout/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/labs/6-Add-Resiliency/src/Basket.API/Basket.API.csproj b/labs/6-Add-Resiliency/src/Basket.API/Basket.API.csproj index 236cf76..eb29e1a 100644 --- a/labs/6-Add-Resiliency/src/Basket.API/Basket.API.csproj +++ b/labs/6-Add-Resiliency/src/Basket.API/Basket.API.csproj @@ -6,7 +6,7 @@ - + diff --git a/labs/6-Add-Resiliency/src/Basket.API/Extensions/HostingExtensions.cs b/labs/6-Add-Resiliency/src/Basket.API/Extensions/HostingExtensions.cs index 8c0c0f4..19c3470 100644 --- a/labs/6-Add-Resiliency/src/Basket.API/Extensions/HostingExtensions.cs +++ b/labs/6-Add-Resiliency/src/Basket.API/Extensions/HostingExtensions.cs @@ -8,7 +8,7 @@ public static void AddApplicationServices(this IHostApplicationBuilder builder) { builder.AddDefaultAuthentication(); - builder.AddRedis("basketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); } diff --git a/labs/6-Add-Resiliency/src/Catalog.API/Properties/launchSettings.json b/labs/6-Add-Resiliency/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/labs/6-Add-Resiliency/src/Catalog.API/Properties/launchSettings.json +++ b/labs/6-Add-Resiliency/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/labs/6-Add-Resiliency/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/labs/6-Add-Resiliency/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/labs/6-Add-Resiliency/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/labs/6-Add-Resiliency/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/6-Add-Resiliency/src/Catalog.Data/Catalog.Data.csproj b/labs/6-Add-Resiliency/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/labs/6-Add-Resiliency/src/Catalog.Data/Catalog.Data.csproj +++ b/labs/6-Add-Resiliency/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/labs/6-Add-Resiliency/src/Keycloak/data/import/eshop-realm.json b/labs/6-Add-Resiliency/src/Keycloak/data/import/eshop-realm.json index 6361cbc..68ecb7e 100644 --- a/labs/6-Add-Resiliency/src/Keycloak/data/import/eshop-realm.json +++ b/labs/6-Add-Resiliency/src/Keycloak/data/import/eshop-realm.json @@ -677,9 +677,9 @@ "clientId" : "webapp", "name" : "eShop Web Frontend", "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", "surrogateAuthRequired" : false, "enabled" : true, "alwaysDisplayInConsole" : false, diff --git a/labs/6-Add-Resiliency/src/Ordering.API/Ordering.API.csproj b/labs/6-Add-Resiliency/src/Ordering.API/Ordering.API.csproj index 71e2879..aad79c7 100644 --- a/labs/6-Add-Resiliency/src/Ordering.API/Ordering.API.csproj +++ b/labs/6-Add-Resiliency/src/Ordering.API/Ordering.API.csproj @@ -15,7 +15,7 @@ - + \ No newline at end of file diff --git a/labs/6-Add-Resiliency/src/Ordering.API/Properties/launchSettings.json b/labs/6-Add-Resiliency/src/Ordering.API/Properties/launchSettings.json index 3d7facd..80b49aa 100644 --- a/labs/6-Add-Resiliency/src/Ordering.API/Properties/launchSettings.json +++ b/labs/6-Add-Resiliency/src/Ordering.API/Properties/launchSettings.json @@ -1,9 +1,18 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5224/", + "applicationUrl": "http://localhost:5224", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7360;http://localhost:5224", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/labs/6-Add-Resiliency/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj b/labs/6-Add-Resiliency/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj index 7fc2650..fbdd23a 100644 --- a/labs/6-Add-Resiliency/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj +++ b/labs/6-Add-Resiliency/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/labs/6-Add-Resiliency/src/Ordering.Data/Ordering.Data.csproj b/labs/6-Add-Resiliency/src/Ordering.Data/Ordering.Data.csproj index 4634c5b..d7f4ed1 100644 --- a/labs/6-Add-Resiliency/src/Ordering.Data/Ordering.Data.csproj +++ b/labs/6-Add-Resiliency/src/Ordering.Data/Ordering.Data.csproj @@ -8,6 +8,6 @@ - + diff --git a/labs/6-Add-Resiliency/src/WebApp/WebApp.csproj b/labs/6-Add-Resiliency/src/WebApp/WebApp.csproj index a260e77..2b8bb1a 100644 --- a/labs/6-Add-Resiliency/src/WebApp/WebApp.csproj +++ b/labs/6-Add-Resiliency/src/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/labs/6-Add-Resiliency/src/eShop.AppHost/KeycloakResource.cs b/labs/6-Add-Resiliency/src/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/labs/6-Add-Resiliency/src/eShop.AppHost/KeycloakResource.cs +++ b/labs/6-Add-Resiliency/src/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/labs/6-Add-Resiliency/src/eShop.AppHost/Program.cs b/labs/6-Add-Resiliency/src/eShop.AppHost/Program.cs index 273d207..0f2560a 100644 --- a/labs/6-Add-Resiliency/src/eShop.AppHost/Program.cs +++ b/labs/6-Add-Resiliency/src/eShop.AppHost/Program.cs @@ -37,20 +37,23 @@ // Apps -var webApp = builder.AddProject("webapp") +// Force HTTPS profile for web app (required for OIDC operations) +var webApp = builder.AddProject("webapp", launchProfileName: "https") .WithReference(basketApi) .WithReference(catalogApi) .WithReference(orderingApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); -idp.WithEnvironment("ORDERINGAPI_HTTP", () => orderingApi.GetEndpoint("http").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); +idp.WithEnvironment("ORDERINGAPI_HTTP", orderingApi.GetEndpoint("http")); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/labs/6-Add-Resiliency/src/eShop.AppHost/Properties/launchSettings.json b/labs/6-Add-Resiliency/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/labs/6-Add-Resiliency/src/eShop.AppHost/Properties/launchSettings.json +++ b/labs/6-Add-Resiliency/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/labs/6-Add-Resiliency/src/eShop.AppHost/eShop.AppHost.csproj b/labs/6-Add-Resiliency/src/eShop.AppHost/eShop.AppHost.csproj index e0bb1c6..d47288f 100644 --- a/labs/6-Add-Resiliency/src/eShop.AppHost/eShop.AppHost.csproj +++ b/labs/6-Add-Resiliency/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/AuthenticationExtensions.cs b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/AuthenticationExtensions.cs index ac0cd81..9e183ad 100644 --- a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -44,7 +44,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/HostingExtensions.cs b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/HostingExtensions.cs index 2b8f4b4..062c139 100644 --- a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -26,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -43,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -68,33 +65,24 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } public static WebApplication UseDefaultExceptionHandler(this WebApplication app, string? errorHandlingPath = null) { + // The developer exception page is used automatically in development if (!app.Environment.IsDevelopment()) { if (errorHandlingPath is not null) @@ -103,6 +91,8 @@ public static WebApplication UseDefaultExceptionHandler(this WebApplication app, } else if (app.Services.GetService() is not null) { + // Default overload of UseExceptionHandler() requires ProblemDetails to be registered which is typically + // only done in API apps so gate on that. app.UseExceptionHandler(); } } @@ -112,24 +102,20 @@ public static WebApplication UseDefaultExceptionHandler(this WebApplication app, public static WebApplication MapDefaultEndpoints(this WebApplication app) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); - - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/OpenApiExtensions.cs b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/OpenApiExtensions.cs index 3b8f852..5c6b693 100644 --- a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, serviceEndPointResolver) => + .Configure((options, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/labs/6-Add-Resiliency/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + + diff --git a/nuget.config b/nuget.config index a495f63..6530e9c 100644 --- a/nuget.config +++ b/nuget.config @@ -2,17 +2,9 @@ - - - - - - diff --git a/src/Basket.API/Basket.API.csproj b/src/Basket.API/Basket.API.csproj index e3d10b1..38fd121 100644 --- a/src/Basket.API/Basket.API.csproj +++ b/src/Basket.API/Basket.API.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Basket.API/Extensions/HostingExtensions.cs b/src/Basket.API/Extensions/HostingExtensions.cs index f8b5c04..19c3470 100644 --- a/src/Basket.API/Extensions/HostingExtensions.cs +++ b/src/Basket.API/Extensions/HostingExtensions.cs @@ -8,7 +8,7 @@ public static void AddApplicationServices(this IHostApplicationBuilder builder) { builder.AddDefaultAuthentication(); - builder.AddRedis("BasketStore"); + builder.AddRedisClient("BasketStore"); builder.Services.AddSingleton(); } diff --git a/src/Basket.API/Properties/launchSettings.json b/src/Basket.API/Properties/launchSettings.json index 3d4f79a..dba6448 100644 --- a/src/Basket.API/Properties/launchSettings.json +++ b/src/Basket.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -8,6 +9,15 @@ "Identity__Url": "http://localhost:5223", "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7278;http://localhost:5221", + "environmentVariables": { + "Identity__Url": "http://localhost:5223", + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/src/Catalog.API/Properties/launchSettings.json b/src/Catalog.API/Properties/launchSettings.json index 71f0437..978e264 100644 --- a/src/Catalog.API/Properties/launchSettings.json +++ b/src/Catalog.API/Properties/launchSettings.json @@ -1,4 +1,5 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", @@ -7,6 +8,14 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7129/;http://localhost:5222/", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj b/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj index c9ef4cd..921f244 100644 --- a/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj +++ b/src/Catalog.Data.Manager/Catalog.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Catalog.Data/Catalog.Data.csproj b/src/Catalog.Data/Catalog.Data.csproj index 9bd24ff..f0147a7 100644 --- a/src/Catalog.Data/Catalog.Data.csproj +++ b/src/Catalog.Data/Catalog.Data.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/IntegrationTests/DistributedApplicationExtensions.cs b/src/IntegrationTests/DistributedApplicationExtensions.cs new file mode 100644 index 0000000..e271ecd --- /dev/null +++ b/src/IntegrationTests/DistributedApplicationExtensions.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Net; +using System.Reflection; +using System.Security.Cryptography; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace IntegrationTests; + +public static partial class DistributedApplicationExtensions +{ + internal const string OutputWriterKey = $"{nameof(DistributedApplicationExtensions)}.OutputWriter"; + + /// + /// Adds a background service to watch resource status changes and optionally logs. + /// + public static IServiceCollection AddResourceWatching(this IServiceCollection services) + { + // Add background service to watch resource status changes and optionally logs + services.AddSingleton(); + services.AddHostedService(sp => sp.GetRequiredService()); + + return services; + } + + /// + /// Configures the builder to write logs to the supplied and store for optional assertion later. + /// + public static TBuilder WriteOutputTo(this TBuilder builder, TextWriter outputWriter) + where TBuilder : IDistributedApplicationTestingBuilder + { + builder.Services.AddResourceWatching(); + + // Add a resource log store to capture logs from resources + builder.Services.AddSingleton(); + + // Configure the builder's logger to redirect it output & store for assertion later + builder.Services.AddKeyedSingleton(OutputWriterKey, outputWriter); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Ensures all parameters in the application configuration have values set. + /// + public static TBuilder WithRandomParameterValues(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + var parameters = builder.Resources.OfType().Where(p => !p.IsConnectionString).ToList(); + foreach (var parameter in parameters) + { + builder.Configuration[$"Parameters:{parameter.Name}"] = parameter.Secret + ? PasswordGenerator.Generate(16, true, true, true, false, 1, 1, 1, 0) + : Convert.ToHexString(RandomNumberGenerator.GetBytes(4)); + } + + return builder; + } + + /// + /// Replaces all named volumes with anonymous volumes so they're isolated across test runs and from the volume the app uses during development. + /// + /// + /// Note that if multiple resources share a volume, the volume will instead be given a random name so that it's still shared across those resources in the test run. + /// + public static TBuilder WithRandomVolumeNames(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + // Named volumes that aren't shared across resources should be replaced with anonymous volumes. + // Named volumes shared by mulitple resources need to have their name randomized but kept shared across those resources. + + // Find all shared volumes and make a map of their original name to a new randomized name + var allResourceNamedVolumes = builder.Resources.SelectMany(r => r.Annotations + .OfType() + .Where(m => m.Type == ContainerMountType.Volume && !string.IsNullOrEmpty(m.Source)) + .Select(m => (Resource: r, Volume: m))) + .ToList(); + var seenVolumes = new HashSet(); + var renamedVolumes = new Dictionary(); + foreach (var resourceVolume in allResourceNamedVolumes) + { + var name = resourceVolume.Volume.Source!; + if (!seenVolumes.Add(name) && !renamedVolumes.ContainsKey(name)) + { + renamedVolumes[name] = $"{name}-{Convert.ToHexString(RandomNumberGenerator.GetBytes(4))}"; + } + } + + // Replace all named volumes with randomly named or anonymous volumes + foreach (var resourceVolume in allResourceNamedVolumes) + { + var resource = resourceVolume.Resource; + var volume = resourceVolume.Volume; + var newName = renamedVolumes.TryGetValue(volume.Source!, out var randomName) ? randomName : null; + var newMount = new ContainerMountAnnotation(newName, volume.Target, ContainerMountType.Volume, volume.IsReadOnly); + resource.Annotations.Remove(volume); + resource.Annotations.Add(newMount); + } + + return builder; + } + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, bool useHttpClientFactory) + => app.CreateHttpClient(resourceName, null, useHttpClientFactory); + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, bool useHttpClientFactory) + { + if (useHttpClientFactory) + { + return app.CreateHttpClient(resourceName, endpointName); + } + + // Don't use the HttpClientFactory to create the HttpClient so, e.g., no resilience policies are applied + var httpClient = new HttpClient + { + BaseAddress = app.GetEndpoint(resourceName, endpointName) + }; + + return httpClient; + } + + /// + /// Creates an configured to communicate with the specified resource with custom configuration. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, Action configure) + { + var services = new ServiceCollection() + .AddHttpClient() + .ConfigureHttpClientDefaults(configure) + .BuildServiceProvider(); + var httpClientFactory = services.GetRequiredService(); + + var httpClient = httpClientFactory.CreateClient(); + httpClient.BaseAddress = app.GetEndpoint(resourceName, endpointName); + + return httpClient; + } + + /// + public static async Task StartAsync(this DistributedApplication app, bool waitForResourcesToStart, CancellationToken cancellationToken = default) + { + var resourceWatcher = app.Services.GetRequiredService(); + var resourcesStartingTask = waitForResourcesToStart ? resourceWatcher.WaitForResourcesToStart() : Task.CompletedTask; + + await app.StartAsync(cancellationToken); + await resourcesStartingTask; + } + + public static LoggerLogStore GetAppHostLogs(this DistributedApplication app) + { + var logStore = app.Services.GetService() + ?? throw new InvalidOperationException($"Log store service was not registered. Ensure the '{nameof(WriteOutputTo)}' method is called before attempting to get AppHost logs."); + return logStore; + } + + /// + /// Gets the logs for all resources in the application. + /// + public static ResourceLogStore GetResourceLogs(this DistributedApplication app) + { + var logStore = app.Services.GetService() + ?? throw new InvalidOperationException($"Log store service was not registered. Ensure the '{nameof(WriteOutputTo)}' method is called before attempting to get resource logs."); ; + return logStore; + } + + /// + /// Attempts to apply EF migrations for the specified project by sending a request to the migrations endpoint /ApplyDatabaseMigrations. + /// + public static async Task TryApplyEfMigrationsAsync(this DistributedApplication app, ProjectResource project) + { + var logger = app.Services.GetRequiredService().CreateLogger(nameof(TryApplyEfMigrationsAsync)); + var projectName = project.GetName(); + + // First check if the project has a migration endpoint, if it doesn't it will respond with a 404 + logger.LogInformation("Checking if project '{ProjectName}' has a migration endpoint", projectName); + using (var checkHttpClient = app.CreateHttpClient(project.Name)) + { + using var emptyDbContextContent = new FormUrlEncodedContent([new("context", "")]); + using var checkResponse = await checkHttpClient.PostAsync("/ApplyDatabaseMigrations", emptyDbContextContent); + if (checkResponse.StatusCode == HttpStatusCode.NotFound) + { + logger.LogInformation("Project '{ProjectName}' does not have a migration endpoint", projectName); + return false; + } + } + + logger.LogInformation("Attempting to apply EF migrations for project '{ProjectName}'", projectName); + + // Load the project assembly and find all DbContext types + var projectDirectory = Path.GetDirectoryName(project.GetProjectMetadata().ProjectPath) ?? throw new UnreachableException(); +#if DEBUG + var configuration = "Debug"; +#else + var configuration = "Release"; +#endif + var projectAssemblyPath = Path.Combine(projectDirectory, "bin", configuration, "net8.0", $"{projectName}.dll"); + var projectAssembly = Assembly.LoadFrom(projectAssemblyPath); + var dbContextTypes = projectAssembly.GetTypes().Where(t => DerivesFromDbContext(t)); + + logger.LogInformation("Found {DbContextCount} DbContext types in project '{ProjectName}'", dbContextTypes.Count(), projectName); + + // Call the migration endpoint for each DbContext type + var migrationsApplied = false; + using var applyMigrationsHttpClient = app.CreateHttpClient(project.Name, useHttpClientFactory: false); + applyMigrationsHttpClient.Timeout = TimeSpan.FromSeconds(240); + foreach (var dbContextType in dbContextTypes) + { + logger.LogInformation("Applying migrations for DbContext '{DbContextType}' in project '{ProjectName}'", dbContextType.FullName, projectName); + using var content = new FormUrlEncodedContent([new("context", dbContextType.AssemblyQualifiedName)]); + using var response = await applyMigrationsHttpClient.PostAsync("/ApplyDatabaseMigrations", content); + if (response.StatusCode == HttpStatusCode.NoContent) + { + migrationsApplied = true; + logger.LogInformation("Migrations applied for DbContext '{DbContextType}' in project '{ProjectName}'", dbContextType.FullName, projectName); + } + } + + return migrationsApplied; + } + + private static bool DerivesFromDbContext(Type type) + { + var baseType = type.BaseType; + + while (baseType is not null) + { + if (baseType.FullName == "Microsoft.EntityFrameworkCore.DbContext" && baseType.Assembly.GetName().Name == "Microsoft.EntityFrameworkCore") + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } +} diff --git a/src/IntegrationTests/IntegrationTests.csproj b/src/IntegrationTests/IntegrationTests.csproj new file mode 100644 index 0000000..9a32489 --- /dev/null +++ b/src/IntegrationTests/IntegrationTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/IntegrationTests/LoggerLogStore.cs b/src/IntegrationTests/LoggerLogStore.cs new file mode 100644 index 0000000..54ba0d9 --- /dev/null +++ b/src/IntegrationTests/LoggerLogStore.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace IntegrationTests; + +/// +/// Stores logs from instances created from . +/// +public class LoggerLogStore(IHostEnvironment hostEnvironment) +{ + private readonly ConcurrentDictionary> _store = []; + + public void AddLog(string category, LogLevel level, string message, Exception? exception) + { + _store.GetOrAdd(category, _ => []).Add((DateTimeOffset.Now, category, level, message, exception)); + } + + public IReadOnlyDictionary> GetLogs() + { + return _store.ToDictionary(entry => entry.Key, entry => (IList<(DateTimeOffset, string, LogLevel, string, Exception?)>)entry.Value); + } + + public void EnsureNoErrors() + { + var logs = GetLogs(); + + var errors = logs.SelectMany(kvp => kvp.Value).Where(log => log.Level == LogLevel.Error || log.Level == LogLevel.Critical).ToList(); + //Where(category => category.Value.Any(log => log.Level == LogLevel.Error || log.Level == LogLevel.Critical)).ToList(); + if (errors.Count > 0) + { + var appName = hostEnvironment.ApplicationName; + throw new InvalidOperationException( + $"AppHost '{appName}' logged errors: {Environment.NewLine}" + + string.Join(Environment.NewLine, errors.Select(log => $"[{log.Category}] {log.Message}"))); + } + } +} diff --git a/src/IntegrationTests/PasswordGenerator.cs b/src/IntegrationTests/PasswordGenerator.cs new file mode 100644 index 0000000..3a2e674 --- /dev/null +++ b/src/IntegrationTests/PasswordGenerator.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Aspire.Hosting.Utils; + +internal static class PasswordGenerator +{ + // Some chars are excluded: + // - prevent potential confusions, e.g., 0,o,O and i,I,l + // - exclude special chars which could interfere with command line arguments, URL (rfc3986 gen-delims), or connection strings, e.g. =,$,... + + internal const string LowerCaseChars = "abcdefghjkmnpqrstuvwxyz"; // exclude i,l,o + internal const string UpperCaseChars = "ABCDEFGHJKMNPQRSTUVWXYZ"; // exclude I,L,O + internal const string NumericChars = "0123456789"; + internal const string SpecialChars = "-_.{}~()*+!"; // exclude &<>=;,`'^%$#@/:[] + + /// + /// Creates a cryptographically random string. + /// + /// + /// The recommended minimum bits of entropy for a generated password is 128 bits. + /// + /// The general calculation of bits of entropy is: + /// + /// log base 2 (numberPossibleOutputs) + /// + /// This generator uses 23 upper case, 23 lower case (excludes i,l,o,I,L,O to prevent confusion), + /// 10 numeric, and 11 special characters. So a total of 67 possible characters. + /// + /// When all character sets are enabled, the number of possible outputs is (67 ^ length). + /// The minimum password length for 128 bits of entropy is 22 characters: log base 2 (67 ^ 22). + /// + /// When character sets are disabled, it lowers the number of possible outputs and thus the bits of entropy. + /// + /// Using minLower, minUpper, minNumeric, and minSpecial also lowers the number of possible outputs and thus the bits of entropy. + /// + /// A generalized lower-bound formula for the number of possible outputs is to consider a string of the form: + /// + /// {nonRequiredCharacters}{requiredCharacters} + /// + /// let a = minLower, b = minUpper, c = minNumeric, d = minSpecial + /// let x = length - (a + b + c + d) + /// + /// nonRequiredPossibilities = 67^x + /// requiredPossibilities = 23^a * 23^b * 10^c * 11^d * (a + b + c + d)! / (a! * b! * c! * d!) + /// + /// lower-bound of total possibilities = nonRequiredPossibilities * requiredPossibilities + /// + /// Putting it all together, the lower-bound bits of entropy calculation is: + /// + /// log base 2 [67^x * 23^a * 23^b * 10^c * 11^d * (a + b + c + d)! / (a! * b! * c! * d!)] + /// + public static string Generate(int minLength, + bool lower, bool upper, bool numeric, bool special, + int minLower, int minUpper, int minNumeric, int minSpecial) + { + ArgumentOutOfRangeException.ThrowIfNegative(minLength); + ArgumentOutOfRangeException.ThrowIfNegative(minLower); + ArgumentOutOfRangeException.ThrowIfNegative(minUpper); + ArgumentOutOfRangeException.ThrowIfNegative(minNumeric); + ArgumentOutOfRangeException.ThrowIfNegative(minSpecial); + CheckMinZeroWhenDisabled(lower, minLower); + CheckMinZeroWhenDisabled(upper, minUpper); + CheckMinZeroWhenDisabled(numeric, minNumeric); + CheckMinZeroWhenDisabled(special, minSpecial); + + var requiredMinLength = checked(minLower + minUpper + minNumeric + minSpecial); + var length = Math.Max(minLength, requiredMinLength); + + Span chars = length <= 128 ? stackalloc char[length] : new char[length]; + + // fill the required characters first + var currentChars = chars; + GenerateRequiredValues(ref currentChars, minLower, LowerCaseChars); + GenerateRequiredValues(ref currentChars, minUpper, UpperCaseChars); + GenerateRequiredValues(ref currentChars, minNumeric, NumericChars); + GenerateRequiredValues(ref currentChars, minSpecial, SpecialChars); + + // fill the rest of the password with random characters from all the available choices + var choices = GetChoices(lower, upper, numeric, special); + RandomNumberGenerator.GetItems(choices, currentChars); + + RandomNumberGenerator.Shuffle(chars); + + var result = new string(chars); + + // clear the buffer so the password isn't in memory in multiple places + chars.Clear(); + + return result; + } + + private static void CheckMinZeroWhenDisabled( + bool enabled, + int minValue, + [CallerArgumentExpression(nameof(enabled))] string? enabledParamName = null, + [CallerArgumentExpression(nameof(minValue))] string? minValueParamName = null) + { + if (!enabled && minValue > 0) + { + ThrowArgumentException(); + } + + void ThrowArgumentException() => throw new ArgumentException($"'{minValueParamName}' must be 0 if '{enabledParamName}' is disabled."); + } + + private static void GenerateRequiredValues(ref Span destination, int minValues, string choices) + { + Debug.Assert(destination.Length >= minValues); + + if (minValues > 0) + { + RandomNumberGenerator.GetItems(choices, destination.Slice(0, minValues)); + destination = destination.Slice(minValues); + } + } + + private static string GetChoices(bool lower, bool upper, bool numeric, bool special) => + (lower, upper, numeric, special) switch + { + (true, true, true, true) => LowerCaseChars + UpperCaseChars + NumericChars + SpecialChars, + (true, true, true, false) => LowerCaseChars + UpperCaseChars + NumericChars, + (true, true, false, true) => LowerCaseChars + UpperCaseChars + SpecialChars, + (true, true, false, false) => LowerCaseChars + UpperCaseChars, + + (true, false, true, true) => LowerCaseChars + NumericChars + SpecialChars, + (true, false, true, false) => LowerCaseChars + NumericChars, + (true, false, false, true) => LowerCaseChars + SpecialChars, + (true, false, false, false) => LowerCaseChars, + + (false, true, true, true) => UpperCaseChars + NumericChars + SpecialChars, + (false, true, true, false) => UpperCaseChars + NumericChars, + (false, true, false, true) => UpperCaseChars + SpecialChars, + (false, true, false, false) => UpperCaseChars, + + (false, false, true, true) => NumericChars + SpecialChars, + (false, false, true, false) => NumericChars, + (false, false, false, true) => SpecialChars, + (false, false, false, false) => throw new ArgumentException("At least one character type must be enabled.") + }; +} diff --git a/src/IntegrationTests/ResourceExtensions.cs b/src/IntegrationTests/ResourceExtensions.cs new file mode 100644 index 0000000..08e3b0c --- /dev/null +++ b/src/IntegrationTests/ResourceExtensions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace IntegrationTests; + +internal static class ResourceExtensions +{ + /// + /// Gets the name of the based on the project file path. + /// + public static string GetName(this ProjectResource project) + { + var metadata = project.GetProjectMetadata(); + return Path.GetFileNameWithoutExtension(metadata.ProjectPath); + } +} diff --git a/src/IntegrationTests/ResourceLogStore.cs b/src/IntegrationTests/ResourceLogStore.cs new file mode 100644 index 0000000..9d7af36 --- /dev/null +++ b/src/IntegrationTests/ResourceLogStore.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace IntegrationTests; + +public class ResourceLogStore +{ + private readonly ConcurrentDictionary> _logs = []; + + internal void Add(IResource resource, IEnumerable logs) + { + _logs.GetOrAdd(resource, _ => []).AddRange(logs); + } + + /// + /// Gets a snapshot of the logs for all resources. + /// + public IReadOnlyDictionary> GetLogs() => + _logs.ToDictionary(entry => entry.Key, entry => (IReadOnlyList)entry.Value); + + /// + /// Gets the logs for the specified resource in the application. + /// + public IReadOnlyList GetLogs(string resourceName) + { + var resource = _logs.Keys.FirstOrDefault(k => string.Equals(k.Name, resourceName, StringComparison.OrdinalIgnoreCase)); + if (resource is not null && _logs.TryGetValue(resource, out var logs)) + { + return logs; + } + return []; + } + + /// + /// Ensures no errors were logged for the specified resource. + /// + public void EnsureNoErrors(string resourceName) + { + EnsureNoErrors(r => string.Equals(r.Name, resourceName, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Ensures no errors were logged for the specified resources. + /// + public void EnsureNoErrors(Func? resourcePredicate = null, bool throwIfNoResourcesMatch = false) + { + var logStore = GetLogs(); + + var resourcesMatched = 0; + foreach (var (resource, logs) in logStore) + { + if (resourcePredicate is null || resourcePredicate(resource)) + { + EnsureNoErrors(resource, logs); + resourcesMatched++; + } + } + + if (throwIfNoResourcesMatch && resourcesMatched == 0 && resourcePredicate is not null) + { + throw new ArgumentException("No resources matched the predicate.", nameof(resourcePredicate)); + } + + static void EnsureNoErrors(IResource resource, IEnumerable logs) + { + var errors = logs.Where(l => l.IsErrorMessage).ToList(); + if (errors.Count > 0) + { + throw new InvalidOperationException($"Resource '{resource.Name}' logged errors: {Environment.NewLine}{string.Join(Environment.NewLine, errors.Select(e => e.Content))}"); + } + } + } +} diff --git a/src/IntegrationTests/ResourceWatcher.cs b/src/IntegrationTests/ResourceWatcher.cs new file mode 100644 index 0000000..cb4a273 --- /dev/null +++ b/src/IntegrationTests/ResourceWatcher.cs @@ -0,0 +1,228 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace IntegrationTests; + +/// +/// A background service that watches for resource start/stop notifications and logs resource state changes. +/// +internal sealed class ResourceWatcher( + DistributedApplicationModel appModel, + ResourceNotificationService resourceNotification, + ResourceLoggerService resourceLoggerService, + IServiceProvider serviceProvider, + ILogger logger) + : BackgroundService +{ + private readonly HashSet _waitingToStartResources = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _startedResources = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _waitingToStopResources = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _stoppedResources = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _resourceState = []; + private readonly TaskCompletionSource _resourcesStartedTcs = new(); + private readonly TaskCompletionSource _resourcesStoppedTcs = new(); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Resource watcher started"); + + var statusWatchableResources = GetStatusWatchableResources().ToList(); + statusWatchableResources.ForEach(r => + { + _waitingToStartResources.Add(r.Name); + _waitingToStopResources.Add(r.Name); + }); + logger.LogInformation("Watching {resourceCount} resources for start/stop changes", statusWatchableResources.Count); + + // We need to pass the stopping token in here because the ResourceNotificationService doesn't stop on host shutdown in preview.5 + await WatchNotifications(stoppingToken); + + logger.LogInformation("Resource watcher stopped"); + } + + public override void Dispose() + { + _resourcesStartedTcs.TrySetException(new DistributedApplicationException("Resource watcher was disposed while waiting for resources to start, likely due to a timeout")); + _resourcesStoppedTcs.TrySetException(new DistributedApplicationException("Resource watcher was disposed while waiting for resources to stop, likely due to a timeout")); + } + + public Task WaitForResourcesToStart() => _resourcesStartedTcs.Task; + + public Task WaitForResourcesToStop() => _resourcesStoppedTcs.Task; + + private async Task WatchNotifications(CancellationToken cancellationToken) + { + var logStore = serviceProvider.GetService(); + var outputWriter = serviceProvider.GetKeyedService(DistributedApplicationExtensions.OutputWriterKey); + var watchingLogs = logStore is not null || outputWriter is not null; + var loggingResourceIds = new HashSet(); + var logWatchTasks = new List(); + + logger.LogInformation("Waiting on {resourcesToStartCount} resources to start", _waitingToStartResources.Count); + + await foreach (var resourceEvent in resourceNotification.WatchAsync().WithCancellation(cancellationToken)) + { + var resourceName = resourceEvent.Resource.Name; + var resourceId = resourceEvent.ResourceId; + + if (watchingLogs && loggingResourceIds.Add(resourceId)) + { + // Start watching the logs for this resource ID + logWatchTasks.Add(WatchResourceLogs(logStore, outputWriter, resourceEvent.Resource, resourceId, cancellationToken)); + } + + _resourceState.TryGetValue(resourceName, out var prevState); + _resourceState[resourceName] = (resourceEvent.Snapshot.State, resourceEvent.Snapshot.ExitCode); + + if (resourceEvent.Snapshot.ExitCode is null && resourceEvent.Snapshot.State is { } newState && !string.IsNullOrEmpty(newState.Text)) + { + if (!string.Equals(prevState.Snapshot?.Text, newState.Text, StringComparison.OrdinalIgnoreCase)) + { + // Log resource state change + logger.LogInformation("Resource '{resourceName}' of type '{resourceType}' changed state: {oldState} -> {newState}", resourceId, resourceEvent.Resource.GetType().Name, prevState.Snapshot?.Text ?? "[null]", newState.Text); + + if (newState.Text.Contains("running", StringComparison.OrdinalIgnoreCase)) + { + // Resource started + HandleResourceStarted(resourceEvent.Resource); + } + else if (newState.Text.Contains("failedtostart", StringComparison.OrdinalIgnoreCase)) + { + // Resource failed to start + HandleResourceStartError(resourceName, $"Resource '{resourceName}' failed to start: {newState.Text}"); + } + else if (newState.Text.Contains("exited", StringComparison.OrdinalIgnoreCase)) + { + if (_waitingToStartResources.Contains(resourceEvent.Resource.Name)) + { + // Resource went straight to exited state + HandleResourceStartError(resourceName, $"Resource '{resourceName}' exited without first running: {newState.Text}"); + } + + // Resource stopped + HandleResourceStopped(resourceEvent.Resource); + } + else if (newState.Text.Contains("starting", StringComparison.OrdinalIgnoreCase) + || newState.Text.Contains("hidden", StringComparison.OrdinalIgnoreCase)) + { + // Resource is still starting + } + else if (!string.IsNullOrEmpty(newState.Text)) + { + logger.LogWarning("Unknown resource state encountered: {state}", newState.Text); + } + } + } + else if (resourceEvent.Snapshot.ExitCode is { } exitCode) + { + if (exitCode != 0) + { + if (_waitingToStartResources.Remove(resourceName)) + { + // Error starting resource + HandleResourceStartError(resourceName, $"Resource '{resourceName}' exited with exit code {exitCode}"); + } + HandleResourceStopError(resourceName, $"Resource '{resourceName}' exited with exit code {exitCode}"); + } + else + { + // Resource exited cleanly + HandleResourceStarted(resourceEvent.Resource, " (exited with code 0)"); + HandleResourceStopped(resourceEvent.Resource, " (exited with code 0)"); + } + } + + if (_waitingToStartResources.Count == 0) + { + logger.LogInformation("All resources started"); + _resourcesStartedTcs.TrySetResult(); + } + + if (_waitingToStopResources.Count == 0) + { + logger.LogInformation("All resources stopped"); + _resourcesStoppedTcs.TrySetResult(); + } + } + + void HandleResourceStartError(string resourceName, string message) + { + if (_waitingToStartResources.Remove(resourceName)) + { + _resourcesStartedTcs.TrySetException(new DistributedApplicationException(message)); + } + _waitingToStopResources.Remove(resourceName); + _stoppedResources.Add(resourceName); + } + + void HandleResourceStopError(string resourceName, string message) + { + _waitingToStartResources.Remove(resourceName); + if (_waitingToStopResources.Remove(resourceName)) + { + _resourcesStoppedTcs.TrySetException(new DistributedApplicationException(message)); + } + _stoppedResources.Add(resourceName); + } + + void HandleResourceStarted(IResource resource, string? suffix = null) + { + if (_waitingToStartResources.Remove(resource.Name) && _startedResources.Add(resource.Name)) + { +#pragma warning disable CA2254 // Template should be a static expression: suffix is not log data + logger.LogInformation($"Resource '{{resourceName}}' started{suffix}", resource.Name); +#pragma warning restore CA2254 + } + + if (_waitingToStartResources.Count > 0) + { + var resourceNames = string.Join(", ", _waitingToStartResources.Select(r => r)); + logger.LogInformation("Still waiting on {resourcesToStartCount} resources to start: {resourcesToStart}", _waitingToStartResources.Count, resourceNames); + } + } + + void HandleResourceStopped(IResource resource, string? suffix = null) + { + if (_waitingToStopResources.Remove(resource.Name) && _stoppedResources.Add(resource.Name)) + { +#pragma warning disable CA2254 // Template should be a static expression: suffix is not log data + logger.LogInformation($"Resource '{{resourceName}}' stopped{suffix}", resource.Name); +#pragma warning restore CA2254 + } + + if (_waitingToStopResources.Count > 0) + { + var resourceNames = string.Join(", ", _waitingToStopResources.Select(r => r)); + logger.LogInformation("Still waiting on {resourcesToStartCount} resources to stop: {resourcesToStart}", _waitingToStopResources.Count, resourceNames); + } + } + + await Task.WhenAll(logWatchTasks); + } + + private async Task WatchResourceLogs(ResourceLogStore? logStore, TextWriter? outputWriter, IResource resource, string resourceId, CancellationToken cancellationToken) + { + if (logStore is not null || outputWriter is not null) + { + await foreach (var logEvent in resourceLoggerService.WatchAsync(resourceId).WithCancellation(cancellationToken)) + { + logStore?.Add(resource, logEvent); + + foreach (var line in logEvent) + { + var kind = line.IsErrorMessage ? "error" : "log"; + outputWriter?.WriteLine("{0} Resource '{1}' {2}: {3}", DateTime.Now.ToString("O"), resource.Name, kind, line.Content); + } + } + } + } + + private IEnumerable GetStatusWatchableResources() + { + return appModel.Resources.Where(r => r is ContainerResource || r is ExecutableResource || r is ProjectResource); + } +} diff --git a/src/IntegrationTests/StoredLogsLogger.cs b/src/IntegrationTests/StoredLogsLogger.cs new file mode 100644 index 0000000..42e1ea5 --- /dev/null +++ b/src/IntegrationTests/StoredLogsLogger.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace IntegrationTests; + +/// +/// A logger that stores logs in a . +/// +/// +/// +/// +internal class StoredLogsLogger(LoggerLogStore logStore, LoggerExternalScopeProvider scopeProvider, string categoryName) : ILogger +{ + public string CategoryName { get; } = categoryName; + + public IDisposable? BeginScope(TState state) where TState : notnull => scopeProvider.Push(state); + + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + logStore.AddLog(CategoryName, logLevel, formatter(state, exception), exception); + } +} diff --git a/src/IntegrationTests/StoredLogsLoggerProvider.cs b/src/IntegrationTests/StoredLogsLoggerProvider.cs new file mode 100644 index 0000000..c673503 --- /dev/null +++ b/src/IntegrationTests/StoredLogsLoggerProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace IntegrationTests; + +/// +/// A logger provider that stores logs in an . +/// +internal class StoredLogsLoggerProvider(LoggerLogStore logStore) : ILoggerProvider +{ + private readonly LoggerExternalScopeProvider _scopeProvider = new(); + + public ILogger CreateLogger(string categoryName) + { + return new StoredLogsLogger(logStore, _scopeProvider, categoryName); + } + + public void Dispose() + { + } +} diff --git a/src/IntegrationTests/WebAppTests.cs b/src/IntegrationTests/WebAppTests.cs new file mode 100644 index 0000000..a38bfbf --- /dev/null +++ b/src/IntegrationTests/WebAppTests.cs @@ -0,0 +1,44 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace IntegrationTests; + +public class WebAppTests(ITestOutputHelper outputHelper) +{ + [Fact] + public async Task GetWebAppUrlsReturnsOkStatusCode() + { + var appHostBuilder = await DistributedApplicationTestingBuilder.CreateAsync(); + appHostBuilder.WriteOutputTo(outputHelper); + appHostBuilder.WithRandomParameterValues(); + appHostBuilder.WithRandomVolumeNames(); + + appHostBuilder.Services.ConfigureHttpClientDefaults(clientBuilder => + { + clientBuilder.ConfigureHttpClient(httpClient => httpClient.Timeout = Timeout.InfiniteTimeSpan); + clientBuilder.AddStandardResilienceHandler(options => + { + options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(2); + options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(1); + options.Retry.BackoffType = Polly.DelayBackoffType.Constant; + options.Retry.Delay = TimeSpan.FromSeconds(5); + options.Retry.MaxRetryAttempts = 30; + options.CircuitBreaker.SamplingDuration = options.AttemptTimeout.Timeout * 2; + }); + }); + + await using var app = await appHostBuilder.BuildAsync(); + await app.StartAsync(waitForResourcesToStart: true); + + var httpClient = app.CreateHttpClient("webapp"); + + var urls = new[] { "/alive", "/health", "/" }; + foreach (var url in urls) + { + var response = await httpClient.GetAsync(url); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } +} \ No newline at end of file diff --git a/src/IntegrationTests/XUnitExtensions.cs b/src/IntegrationTests/XUnitExtensions.cs new file mode 100644 index 0000000..6d0b2a3 --- /dev/null +++ b/src/IntegrationTests/XUnitExtensions.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace IntegrationTests; + +internal static partial class DistributedApplicationTestFactory +{ + ///// + ///// Creates an for the specified app host assembly and outputs logs to the provided . + ///// + //public static async Task CreateAsync(string appHostAssemblyPath, ITestOutputHelper testOutputHelper) + //{ + // var builder = await CreateAsync(appHostAssemblyPath, new XUnitTextWriter(testOutputHelper)); + // builder.Services.AddSingleton(); + // builder.Services.AddSingleton(testOutputHelper); + // return builder; + //} + + /// + /// Writes messages and resource logs to the provided . + /// + /// The builder. + /// The output. + /// The builder. + public static IDistributedApplicationTestingBuilder WriteOutputTo(this IDistributedApplicationTestingBuilder builder, ITestOutputHelper testOutputHelper) + { + // Enable the core ILogger and resource output capturing + builder.WriteOutputTo(new XUnitTextWriter(testOutputHelper)); + + // Enable ILogger going to xUnit output + builder.Services.AddSingleton(testOutputHelper); + builder.Services.AddSingleton(); + + return builder; + } +} diff --git a/src/IntegrationTests/XUnitLogger.cs b/src/IntegrationTests/XUnitLogger.cs new file mode 100644 index 0000000..0c8f665 --- /dev/null +++ b/src/IntegrationTests/XUnitLogger.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace IntegrationTests; + +/// +/// An that writes log messages to an . +/// +internal class XUnitLogger(ITestOutputHelper testOutputHelper, LoggerExternalScopeProvider scopeProvider, string categoryName) : ILogger +{ + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public IDisposable? BeginScope(TState state) where TState : notnull => scopeProvider.Push(state); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var sb = new StringBuilder(); + + sb.Append(DateTime.Now.ToString("O")).Append(' ') + .Append(GetLogLevelString(logLevel)) + .Append(" [").Append(categoryName).Append("] ") + .Append(formatter(state, exception)); + + if (exception is not null) + { + sb.AppendLine().Append(exception); + } + + // Append scopes + scopeProvider.ForEachScope((scope, state) => + { + state.AppendLine(); + state.Append(" => "); + state.Append(scope); + }, sb); + + testOutputHelper.WriteLine(sb.ToString()); + } + + private static string GetLogLevelString(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => "trce", + LogLevel.Debug => "dbug", + LogLevel.Information => "info", + LogLevel.Warning => "warn", + LogLevel.Error => "fail", + LogLevel.Critical => "crit", + _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) + }; + } +} diff --git a/src/IntegrationTests/XUnitLoggerProvider.cs b/src/IntegrationTests/XUnitLoggerProvider.cs new file mode 100644 index 0000000..93fd7cc --- /dev/null +++ b/src/IntegrationTests/XUnitLoggerProvider.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace IntegrationTests; + +/// +/// An that creates instances that output to the supplied . +/// +internal class XUnitLoggerProvider(ITestOutputHelper testOutputHelper) : ILoggerProvider +{ + private readonly LoggerExternalScopeProvider _scopeProvider = new(); + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(testOutputHelper, _scopeProvider, categoryName); + } + + public void Dispose() + { + } +} diff --git a/src/IntegrationTests/XUnitTextWriter.cs b/src/IntegrationTests/XUnitTextWriter.cs new file mode 100644 index 0000000..f7c741f --- /dev/null +++ b/src/IntegrationTests/XUnitTextWriter.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Xunit.Abstractions; + +namespace IntegrationTests; + +/// +/// A that writes to an . +/// +internal class XUnitTextWriter(ITestOutputHelper output) : TextWriter +{ + private readonly StringBuilder _sb = new(); + + public override Encoding Encoding => Encoding.Unicode; + + public override void Write(char value) + { + if (value == '\r' || value == '\n') + { + if (_sb.Length > 0) + { + output.WriteLine(_sb.ToString()); + _sb.Clear(); + } + } + else + { + _sb.Append(value); + } + } +} diff --git a/src/Keycloak/data/import/eshop-realm.json b/src/Keycloak/data/import/eshop-realm.json index 6361cbc..6f026d9 100644 --- a/src/Keycloak/data/import/eshop-realm.json +++ b/src/Keycloak/data/import/eshop-realm.json @@ -436,328 +436,345 @@ "roles" : [ "manage-account", "view-groups" ] } ] }, - "clients" : [ { - "id" : "82385f82-f986-49fe-a512-5a8ea45f09ee", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/eShop/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/eShop/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "d84cf061-eeeb-4675-b0d0-5cd609bc44c6", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/eShop/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/eShop/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + "clients": [ + { + "id": "82385f82-f986-49fe-a512-5a8ea45f09ee", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/eShop/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ "/realms/eShop/account/*" ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "6abcbb09-2122-4bbb-91f4-4c61c8abff65", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "f63db859-cf66-42f4-9ce0-1d40ca5c922c", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" + { + "id": "d84cf061-eeeb-4675-b0d0-5cd609bc44c6", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/eShop/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ "/realms/eShop/account/*" ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "6abcbb09-2122-4bbb-91f4-4c61c8abff65", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "1790c30e-7010-4d4f-bc3b-181a65868873", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" + { + "id": "f63db859-cf66-42f4-9ce0-1d40ca5c922c", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c", - "clientId" : "orderingswaggerui", - "name" : "Ordering Swagger UI", - "description" : "", - "rootUrl" : "${ORDERINGAPI_HTTP}", - "adminUrl" : "${ORDERINGAPI_HTTP}", - "baseUrl" : "${ORDERINGAPI_HTTP}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "${ORDERINGAPI_HTTP}/*" ], - "webOrigins" : [ "${ORDERINGAPI_HTTP}" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : true, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : true, - "protocol" : "openid-connect", - "attributes" : { - "oidc.ciba.grant.enabled" : "false", - "post.logout.redirect.uris" : "+", - "oauth2.device.authorization.grant.enabled" : "false", - "backchannel.logout.session.required" : "true", - "backchannel.logout.revoke.offline.tokens" : "false" + { + "id": "1790c30e-7010-4d4f-bc3b-181a65868873", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : -1, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+" + { + "id": "6bbe9167-4ac5-49e3-a0ea-06fa6b9fe56c", + "clientId": "orderingswaggerui", + "name": "Ordering Swagger UI", + "description": "", + "rootUrl": "${ORDERINGAPI_HTTP}", + "adminUrl": "${ORDERINGAPI_HTTP}", + "baseUrl": "${ORDERINGAPI_HTTP}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ "${ORDERINGAPI_HTTP}/*" ], + "webOrigins": [ "${ORDERINGAPI_HTTP}" ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "e6a9aea6-f8d4-40f6-a832-6537fce8791e", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/eShop/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/eShop/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" + { + "id": "348d0c1d-6d87-4975-b5b1-d3f7ca245cd0", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "ad67051f-d487-417e-9375-f6563ee86ddf", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - }, { - "id" : "cc5ff175-d0b3-4759-8b01-49e60dfa9269", - "clientId" : "webapp", - "name" : "eShop Web Frontend", - "description" : "The frontend web site of the eShop system.", - "rootUrl": "${WEBAPP_HTTP}", - "adminUrl": "${WEBAPP_HTTP}", - "baseUrl": "${WEBAPP_HTTP}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "secret" : "dAayhA7hWQFrNpKJvskRodHSDuf1burR", - "redirectUris": [ "${WEBAPP_HTTP}/*", "${WEBAPP_HTTPS}/*" ], - "webOrigins": [ "${WEBAPP_HTTPS}", "${WEBAPP_HTTP}" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : true, - "publicClient" : false, - "frontchannelLogout" : true, - "protocol" : "openid-connect", - "attributes" : { - "oidc.ciba.grant.enabled" : "false", - "client.secret.creation.time" : "1705700546", - "backchannel.logout.session.required" : "true", - "post.logout.redirect.uris" : "+", - "oauth2.device.authorization.grant.enabled" : "false", - "display.on.consent.screen" : "false", - "backchannel.logout.revoke.offline.tokens" : "false" + { + "id": "e6a9aea6-f8d4-40f6-a832-6537fce8791e", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/eShop/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ "/admin/eShop/console/*" ], + "webOrigins": [ "+" ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "ad67051f-d487-417e-9375-f6563ee86ddf", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : -1, - "protocolMappers" : [ { - "id" : "46526429-fa70-4518-9512-089a9830f179", - "name" : "Client Host", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientHost", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientHost", - "jsonType.label" : "String" - } - }, { - "id" : "9eee2065-3d31-4621-be61-b83f05f2c113", - "name" : "Client ID", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "client_id", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "client_id", - "jsonType.label" : "String" - } - }, { - "id" : "4951c816-a177-4193-b714-585b0bb23ab5", - "name" : "Client IP Address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientAddress", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientAddress", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] - } ], + { + "id": "cc5ff175-d0b3-4759-8b01-49e60dfa9269", + "clientId": "webapp", + "name": "eShop Web Frontend", + "description": "The frontend web site of the eShop system.", + "rootUrl": "${WEBAPP_HTTPS}", + "adminUrl": "${WEBAPP_HTTPS_CONTAINERHOST}", + "baseUrl": "${WEBAPP_HTTPS}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "${WEBAPP_CLIENT_SECRET}", + "redirectUris": [ "${WEBAPP_HTTP}/*", "${WEBAPP_HTTPS}/*" ], + "webOrigins": [ "${WEBAPP_HTTPS}", "${WEBAPP_HTTP}" ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1705700546", + "backchannel.logout.session.required": "true", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "display.on.consent.screen": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "46526429-fa70-4518-9512-089a9830f179", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "9eee2065-3d31-4621-be61-b83f05f2c113", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "id": "4951c816-a177-4193-b714-585b0bb23ab5", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes": [ "address", "phone", "offline_access", "microprofile-jwt" ] + } + ], "clientScopes" : [ { "id" : "4d6f4264-5a7e-4d41-894c-6b721f14fd1f", "name" : "address", diff --git a/src/Ordering.API/Ordering.API.csproj b/src/Ordering.API/Ordering.API.csproj index 71e2879..aad79c7 100644 --- a/src/Ordering.API/Ordering.API.csproj +++ b/src/Ordering.API/Ordering.API.csproj @@ -15,7 +15,7 @@ - + \ No newline at end of file diff --git a/src/Ordering.API/Properties/launchSettings.json b/src/Ordering.API/Properties/launchSettings.json index 3d7facd..80b49aa 100644 --- a/src/Ordering.API/Properties/launchSettings.json +++ b/src/Ordering.API/Properties/launchSettings.json @@ -1,9 +1,18 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "launchBrowser": true, - "applicationUrl": "http://localhost:5224/", + "applicationUrl": "http://localhost:5224", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7360;http://localhost:5224", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj b/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj index 7fc2650..fbdd23a 100644 --- a/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj +++ b/src/Ordering.Data.Manager/Ordering.Data.Manager.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Ordering.Data/Ordering.Data.csproj b/src/Ordering.Data/Ordering.Data.csproj index 4634c5b..d7f4ed1 100644 --- a/src/Ordering.Data/Ordering.Data.csproj +++ b/src/Ordering.Data/Ordering.Data.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/WebApp/Extensions/HostingExtensions.cs b/src/WebApp/Extensions/HostingExtensions.cs index 0fdeaf4..eb78ea4 100644 --- a/src/WebApp/Extensions/HostingExtensions.cs +++ b/src/WebApp/Extensions/HostingExtensions.cs @@ -27,13 +27,13 @@ public static void AddApplicationServices(this IHostApplicationBuilder builder) builder.Services.AddSingleton(); // HTTP and gRPC client registrations - builder.Services.AddGrpcClient(o => o.Address = new("http://basket-api")) + builder.Services.AddGrpcClient(o => o.Address = new("https://basket-api")) .AddAuthToken(); - builder.Services.AddHttpClient(o => o.BaseAddress = new("http://catalog-api")) + builder.Services.AddHttpClient(o => o.BaseAddress = new("https+http://catalog-api")) .AddAuthToken(); - builder.Services.AddHttpClient(o => o.BaseAddress = new("http://ordering-api")) + builder.Services.AddHttpClient(o => o.BaseAddress = new("https+http://ordering-api")) .AddAuthToken(); builder.Services.AddHttpClient(OpenIdConnectBackchannel, o => o.BaseAddress = new("http://idp")); diff --git a/src/WebApp/Program.cs b/src/WebApp/Program.cs index d6c7dc7..099bda4 100644 --- a/src/WebApp/Program.cs +++ b/src/WebApp/Program.cs @@ -30,6 +30,6 @@ app.MapRazorComponents().AddInteractiveServerRenderMode(); -app.MapForwarder("/product-images/{id}", "http://catalog-api", "/api/v1/catalog/items/{id}/pic"); +app.MapForwarder("/product-images/{id}", "https+http://catalog-api", "/api/v1/catalog/items/{id}/pic"); app.Run(); diff --git a/src/WebApp/WebApp.csproj b/src/WebApp/WebApp.csproj index a260e77..2b8bb1a 100644 --- a/src/WebApp/WebApp.csproj +++ b/src/WebApp/WebApp.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/eShop.AppHost/KeycloakResource.cs b/src/eShop.AppHost/KeycloakResource.cs index c6515c6..0d18341 100644 --- a/src/eShop.AppHost/KeycloakResource.cs +++ b/src/eShop.AppHost/KeycloakResource.cs @@ -6,57 +6,72 @@ internal static class KeycloakHostingExtensions { private const int DefaultContainerPort = 8080; - public static IResourceBuilder AddKeycloakContainer( + public static IResourceBuilder WithReference(this IResourceBuilder builder, + IResourceBuilder keycloakBuilder, + string env) + where TResource : IResourceWithEnvironment + { + builder.WithReference(keycloakBuilder); + builder.WithEnvironment(env, keycloakBuilder.Resource.ClientSecret); + + return builder; + } + + public static IResourceBuilder AddKeycloakContainer( this IDistributedApplicationBuilder builder, string name, int? port = null, string? tag = null) { - var keycloakContainer = new KeycloakContainerResource(name); + var keycloakContainer = new KeycloakResource(name) + { + ClientSecret = Guid.NewGuid().ToString("N") + }; - return builder + var keycloak = builder .AddResource(keycloakContainer) .WithAnnotation(new ContainerImageAnnotation { Registry = "quay.io", Image = "keycloak/keycloak", Tag = tag ?? "latest" }) - .WithHttpEndpoint(hostPort: port, containerPort: DefaultContainerPort) + .WithHttpEndpoint(port: port, targetPort: DefaultContainerPort) .WithEnvironment("KEYCLOAK_ADMIN", "admin") .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", "admin") - .WithArgs("start-dev") - .WithManifestPublishingCallback(context => WriteKeycloakContainerToManifest(context, keycloakContainer)); + .WithEnvironment("WEBAPP_CLIENT_SECRET", keycloakContainer.ClientSecret); + + if (builder.ExecutionContext.IsRunMode) + { + keycloak.WithArgs("start-dev"); + } + else + { + keycloak.WithArgs("start"); + } + + return keycloak; } - public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) + public static IResourceBuilder ImportRealms(this IResourceBuilder builder, string source) { builder - .WithVolumeMount(source, "/opt/keycloak/data/import", VolumeMountType.Bind) - .WithAnnotation(new ExecutableArgsCallbackAnnotation(args => + .WithBindMount(source, "/opt/keycloak/data/import") + .WithAnnotation(new CommandLineArgsCallbackAnnotation(args => { + // TODO: This could be cleaned up to make it properly compose with any other callers who customize args args.Clear(); - args.Add("start-dev"); + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + args.Add("start-dev"); + } + else + { + args.Add("start"); + } args.Add("--import-realm"); })); return builder; } - - private static void WriteKeycloakContainerToManifest(ManifestPublishingContext context, KeycloakContainerResource resource) - { - var manifestResource = new KeycloakContainerResource(resource.Name); - - foreach (var annotation in resource.Annotations) - { - if (annotation is not ExecutableArgsCallbackAnnotation) - { - manifestResource.Annotations.Add(annotation); - } - } - - // Set the container entry point to 'start' instead of 'start-dev' - manifestResource.Annotations.Add(new ExecutableArgsCallbackAnnotation(args => args.Add("start"))); - - context.WriteContainer(resource); - } } -internal class KeycloakContainerResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery +internal class KeycloakResource(string name) : ContainerResource(name), IResourceWithServiceDiscovery { + public string? ClientSecret { get; set; } } diff --git a/src/eShop.AppHost/Program.cs b/src/eShop.AppHost/Program.cs index 273d207..694ee6e 100644 --- a/src/eShop.AppHost/Program.cs +++ b/src/eShop.AppHost/Program.cs @@ -4,8 +4,10 @@ // Databases -var basketStore = builder.AddRedis("BasketStore").WithRedisCommander(); -var postgres = builder.AddPostgres("postgres").WithPgAdmin(); +var basketStore = builder.AddRedis("BasketStore") + .WithRedisCommander(); +var postgres = builder.AddPostgres("postgres") + .WithPgAdmin(); var catalogDb = postgres.AddDatabase("CatalogDB"); var orderDb = postgres.AddDatabase("OrderingDB"); @@ -41,16 +43,27 @@ .WithReference(basketApi) .WithReference(catalogApi) .WithReference(orderingApi) - .WithReference(idp) - // Force HTTPS profile for web app (required for OIDC operations) - .WithLaunchProfile("https"); + .WithReference(idp, env: "Identity__ClientSecret"); // Inject the project URLs for Keycloak realm configuration -idp.WithEnvironment("WEBAPP_HTTP", () => webApp.GetEndpoint("http").UriString); -idp.WithEnvironment("WEBAPP_HTTPS", () => webApp.GetEndpoint("https").UriString); -idp.WithEnvironment("ORDERINGAPI_HTTP", () => orderingApi.GetEndpoint("http").UriString); +var webAppHttp = webApp.GetEndpoint("http"); +var webAppHttps = webApp.GetEndpoint("https"); +idp.WithEnvironment("WEBAPP_HTTP_CONTAINERHOST", webAppHttp); +idp.WithEnvironment("WEBAPP_HTTP", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +if (webAppHttps.Exists) +{ + idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttps); + idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttps.Scheme}://{webAppHttps.Host}:{webAppHttps.Port}"); +} +else +{ + // Still need to set these environment variables so the KeyCloak realm import doesn't fail + idp.WithEnvironment("WEBAPP_HTTPS_CONTAINERHOST", webAppHttp); + idp.WithEnvironment("WEBAPP_HTTPS", () => $"{webAppHttp.Scheme}://{webAppHttp.Host}:{webAppHttp.Port}"); +} +idp.WithEnvironment("ORDERINGAPI_HTTP", orderingApi.GetEndpoint("http")); // Inject assigned URLs for Catalog API -catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", () => catalogApi.GetEndpoint("http").UriString); +catalogApi.WithEnvironment("CatalogOptions__PicBaseAddress", catalogApi.GetEndpoint("http")); builder.Build().Run(); diff --git a/src/eShop.AppHost/Properties/launchSettings.json b/src/eShop.AppHost/Properties/launchSettings.json index 84439ec..ae3ace6 100644 --- a/src/eShop.AppHost/Properties/launchSettings.json +++ b/src/eShop.AppHost/Properties/launchSettings.json @@ -1,31 +1,29 @@ { + "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "https://localhost:17219;http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "http", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16119" - }, - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:18848" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21023", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22616" + } }, - "https": { + "http": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "", + "applicationUrl": "http://localhost:15178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", - "DOTNET_LAUNCH_PROFILE": "https", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:18076" - }, - "dotnetRunMessages": true, - "applicationUrl": "https://localhost:19888" + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19076", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023" + } } - }, - "$schema": "http://json.schemastore.org/launchsettings.json" + } } diff --git a/src/eShop.AppHost/eShop.AppHost.csproj b/src/eShop.AppHost/eShop.AppHost.csproj index e0bb1c6..d47288f 100644 --- a/src/eShop.AppHost/eShop.AppHost.csproj +++ b/src/eShop.AppHost/eShop.AppHost.csproj @@ -10,7 +10,9 @@ - + + + diff --git a/src/eShop.ServiceDefaults/AuthenticationExtensions.cs b/src/eShop.ServiceDefaults/AuthenticationExtensions.cs index ac0cd81..9e183ad 100644 --- a/src/eShop.ServiceDefaults/AuthenticationExtensions.cs +++ b/src/eShop.ServiceDefaults/AuthenticationExtensions.cs @@ -44,7 +44,7 @@ public static Uri GetIdpAuthorityUri(this HttpClient httpClient, IConfiguration return identityUri; } - public static Uri ResolveIdpAuthorityUri(this ServiceEndPointResolverRegistry resolver, IConfiguration configuration, string serviceName = "http://idp") + public static Uri ResolveIdpAuthorityUri(this ServiceEndpointResolver resolver, IConfiguration configuration, string serviceName = "http://idp") { // Sync over async :( var idpBaseUrl = resolver.ResolveEndPointUrlAsync(serviceName).AsTask().GetAwaiter().GetResult() diff --git a/src/eShop.ServiceDefaults/HostingExtensions.cs b/src/eShop.ServiceDefaults/HostingExtensions.cs index 06debf2..062c139 100644 --- a/src/eShop.ServiceDefaults/HostingExtensions.cs +++ b/src/eShop.ServiceDefaults/HostingExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; -using OpenTelemetry.Logs; +using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -26,7 +26,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); return builder; @@ -43,17 +43,14 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { - metrics.AddRuntimeInstrumentation() - .AddBuiltInMeters(); + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); }) .WithTracing(tracing => { - if (builder.Environment.IsDevelopment()) - { - tracing.SetSampler(new AlwaysOnSampler()); - } - tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); @@ -68,27 +65,17 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli if (useOtlpExporter) { - builder.Services.Configure(logging => logging.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); - builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + builder.Services.AddOpenTelemetry().UseOtlpExporter(); } - // Configure alternative exporters - //builder.Services.AddOpenTelemetry() - // .WithMetrics(metrics => - // { - // // Uncomment the following line to enable the Prometheus endpoint - // //metrics.AddPrometheusExporter(); - // }); - return builder; } public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) { builder.Services.AddHealthChecks() - // Add a default liveness check to ensure app is responsive - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); return builder; } @@ -115,24 +102,20 @@ public static WebApplication UseDefaultExceptionHandler(this WebApplication app, public static WebApplication MapDefaultEndpoints(this WebApplication app) { - // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) - // app.MapPrometheusScrapingEndpoint(); - - // All health checks must pass for app to be considered ready to accept traffic after starting - app.MapHealthChecks("/health"); - - // Only health checks tagged with the "live" tag must pass for app to be considered alive - app.MapHealthChecks("/alive", new HealthCheckOptions + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) { - Predicate = r => r.Tags.Contains("live") - }); + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } return app; } - - private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => - meterProviderBuilder.AddMeter( - "Microsoft.AspNetCore.Hosting", - "Microsoft.AspNetCore.Server.Kestrel", - "System.Net.Http"); } diff --git a/src/eShop.ServiceDefaults/OpenApiExtensions.cs b/src/eShop.ServiceDefaults/OpenApiExtensions.cs index 3b8f852..5c6b693 100644 --- a/src/eShop.ServiceDefaults/OpenApiExtensions.cs +++ b/src/eShop.ServiceDefaults/OpenApiExtensions.cs @@ -74,7 +74,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui services.AddSwaggerGen(); services.AddOptions() - .Configure((options, serviceEndPointResolver) => + .Configure((options, ServiceEndpointResolver) => { /// { /// "OpenApi": { @@ -113,7 +113,7 @@ public static IHostApplicationBuilder AddDefaultOpenApi(this IHostApplicationBui // } // } - var identityUri = serviceEndPointResolver.ResolveIdpAuthorityUri(configuration); + var identityUri = ServiceEndpointResolver.ResolveIdpAuthorityUri(configuration); var scopes = identitySection.GetSection("Scopes").GetChildren().ToDictionary(p => p.Key, p => p.Value); diff --git a/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs b/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs index ae1762d..ee6c765 100644 --- a/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs +++ b/src/eShop.ServiceDefaults/ServiceDiscoveryExtensions.cs @@ -2,13 +2,13 @@ public static class ServiceDiscoveryExtensions { - public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndPointResolverRegistry resolver, string serviceName, CancellationToken cancellationToken = default) + public static async ValueTask ResolveEndPointUrlAsync(this ServiceEndpointResolver resolver, string serviceName, CancellationToken cancellationToken = default) { var scheme = ExtractScheme(serviceName); - var endpoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken); - if (endpoints.Count > 0) + var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken); + if (endpoints.Endpoints.Count > 0) { - var address = endpoints[0].GetEndPointString(); + var address = endpoints.Endpoints[0].ToString(); return $"{scheme}://{address}"; } return null; diff --git a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj index e86eaa1..f1238a6 100644 --- a/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj +++ b/src/eShop.ServiceDefaults/eShop.ServiceDefaults.csproj @@ -16,13 +16,13 @@ - + - - - - - - + + + + + +