diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index 261dedd9b..01698cb72 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -25,6 +25,14 @@ var app = builder.Build(); +// Add lexbox-version header to all requests +app.Logger.LogInformation("FwHeadless version: {version}", AppVersionService.Version); +app.Use(async (context, next) => +{ + context.Response.Headers["lexbox-version"] = AppVersionService.Version; + await next(); +}); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { diff --git a/backend/FwHeadless/Services/AppVersionService.cs b/backend/FwHeadless/Services/AppVersionService.cs new file mode 100644 index 000000000..4bc753899 --- /dev/null +++ b/backend/FwHeadless/Services/AppVersionService.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace FwHeadless; + +public static class AppVersionService +{ + public static readonly string Version = typeof(AppVersionService).Assembly + .GetCustomAttribute()?.InformationalVersion ?? "dev"; +} diff --git a/backend/LexBoxApi/Config/HealthChecksConfig.cs b/backend/LexBoxApi/Config/HealthChecksConfig.cs new file mode 100644 index 000000000..5e6d6cfb6 --- /dev/null +++ b/backend/LexBoxApi/Config/HealthChecksConfig.cs @@ -0,0 +1,7 @@ +namespace LexBoxApi.Config; + +public class HealthChecksConfig +{ + public bool RequireFwHeadlessContainerVersionMatch { get; init; } = true; + public bool RequireHealthyFwHeadlessContainer { get; init; } = true; +} diff --git a/backend/LexBoxApi/LexBoxKernel.cs b/backend/LexBoxApi/LexBoxKernel.cs index 7b420421c..69bbed341 100644 --- a/backend/LexBoxApi/LexBoxKernel.cs +++ b/backend/LexBoxApi/LexBoxKernel.cs @@ -44,6 +44,10 @@ public static void AddLexBoxApi(this IServiceCollection services, .BindConfiguration("Tus") .ValidateDataAnnotations() .ValidateOnStart(); + services.AddOptions() + .BindConfiguration("HealthChecks") + .ValidateDataAnnotations() + .ValidateOnStart(); services.AddHttpClient(); services.AddHttpContextAccessor(); services.AddMemoryCache(); @@ -57,6 +61,7 @@ public static void AddLexBoxApi(this IServiceCollection services, services.AddScoped(); services.AddHostedService(); services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddResiliencePipeline>(IsLanguageForgeProjectDataLoader.ResiliencePolicyName, (builder, context) => { @@ -69,7 +74,9 @@ public static void AddLexBoxApi(this IServiceCollection services, if (environment.IsDevelopment()) services.AddHostedService(); services.AddScheduledTasks(configuration); - services.AddHealthChecks().AddCheck("hgweb", HealthStatus.Unhealthy, ["hg"], TimeSpan.FromSeconds(5)); + services.AddHealthChecks() + .AddCheck("hgweb", HealthStatus.Unhealthy, ["hg"], TimeSpan.FromSeconds(5)) + .AddCheck("fw-headless", HealthStatus.Unhealthy, ["fw-headless"], TimeSpan.FromSeconds(5)); services.AddSyncProxy(); AuthKernel.AddLexBoxAuth(services, configuration, environment); services.AddLexGraphQL(environment); diff --git a/backend/LexBoxApi/Services/FwHeadlessHealthCheck.cs b/backend/LexBoxApi/Services/FwHeadlessHealthCheck.cs new file mode 100644 index 000000000..8ee03fb7b --- /dev/null +++ b/backend/LexBoxApi/Services/FwHeadlessHealthCheck.cs @@ -0,0 +1,37 @@ +using LexBoxApi.Config; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace LexBoxApi.Services; + +public class FwHeadlessHealthCheck(IHttpClientFactory clientFactory, IOptions healthCheckOptions) : IHealthCheck +{ + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = new()) + { + var http = clientFactory.CreateClient(); + var fwHeadlessResponse = await http.GetAsync("http://fw-headless/api/healthz"); + if (!fwHeadlessResponse.IsSuccessStatusCode) + { + if (healthCheckOptions.Value.RequireHealthyFwHeadlessContainer) + { + return HealthCheckResult.Unhealthy("fw-headless not repsonding to health check"); + } + else + { + return HealthCheckResult.Degraded("fw-headless not repsonding to health check"); + } + } + var fwHeadlessVersion = fwHeadlessResponse.Headers.GetValues("lexbox-version").FirstOrDefault(); + if (healthCheckOptions.Value.RequireFwHeadlessContainerVersionMatch && string.IsNullOrEmpty(fwHeadlessVersion)) + { + return HealthCheckResult.Degraded("fw-headless version check failed to return a value"); + } + if (healthCheckOptions.Value.RequireFwHeadlessContainerVersionMatch && fwHeadlessVersion != AppVersionService.Version) + { + return HealthCheckResult.Degraded( + $"api version: '{AppVersionService.Version}' fw-headless version: '{fwHeadlessVersion}' mismatch"); + } + return HealthCheckResult.Healthy(); + } +} diff --git a/backend/LexBoxApi/appsettings.Development.json b/backend/LexBoxApi/appsettings.Development.json index fe5d83396..995c9d35a 100644 --- a/backend/LexBoxApi/appsettings.Development.json +++ b/backend/LexBoxApi/appsettings.Development.json @@ -49,6 +49,10 @@ "LfMergeTrustToken": "lf-merge-dev-trust-token", "AutoUpdateLexEntryCountOnSendReceive": true }, + "HealthChecks": { + "RequireFwHeadlessContainerVersionMatch": false, + "RequireHealthyFwHeadlessContainer": false + }, "Authentication": { "Jwt": { "Secret": "d5cf1adc-16e6-4064-8041-4cfa00174210" diff --git a/deployment/develop/lexbox-deployment.patch.yaml b/deployment/develop/lexbox-deployment.patch.yaml index 19ce160dd..0e1db473d 100644 --- a/deployment/develop/lexbox-deployment.patch.yaml +++ b/deployment/develop/lexbox-deployment.patch.yaml @@ -26,3 +26,5 @@ spec: value: "https://develop.lexbox.org" - name: HgConfig__RequireContainerVersionMatch value: "false" + - name: HealthChecksConfig__RequireFwHeadlessContainerVersionMatch + value: "false" diff --git a/deployment/local-dev/lexbox-deployment.patch.yaml b/deployment/local-dev/lexbox-deployment.patch.yaml index f6a8fe034..9d2f83656 100644 --- a/deployment/local-dev/lexbox-deployment.patch.yaml +++ b/deployment/local-dev/lexbox-deployment.patch.yaml @@ -36,6 +36,10 @@ spec: valueFrom: - name: CloudFlare__AllowDomain value: "mailinator.com" + - name: HealthChecksConfig__RequireFwHeadlessContainerVersionMatch + value: "false" + - name: HealthChecksConfig__RequireHealthyFwHeadlessContainer + value: "false" - name: Email__SmtpUser value: 'maildev' valueFrom: