From 1ccd9dbd250e1d816aee6a87f90874c2b350f64a Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Thu, 24 Jul 2025 19:36:13 -0500 Subject: [PATCH 01/14] Add quartz.NET --- Application/Ed-Fi-ODS-AdminApi.sln | 10 ++ .../Ed-Fi-ODS-AdminApi.sln.DotSettings | 5 + .../AdminApiSettings.cs | 34 ++++ .../AppSettings.cs | 17 ++ .../Application.cs | 101 ++++++++++++ ...dFi.AdminConsole.HealthCheckService.csproj | 47 ++++++ ...dFi.AdminConsole.HealthCheckService.nuspec | 17 ++ .../Extensions/HttpStatusCode.cs | 25 +++ .../Features/AdminApi/AdminApiCaller.cs | 115 ++++++++++++++ .../Features/AdminApi/AdminApiClient.cs | 121 +++++++++++++++ .../AdminApi/AdminApiHealthCheckPost.cs | 16 ++ .../Features/AdminApi/AdminConsoleInstance.cs | 40 +++++ .../Features/AdminApi/AdminConsoleTenant.cs | 22 +++ .../OdsApi/AppSettingsOdsApiEndpoints.cs | 35 +++++ .../Features/OdsApi/OdsApiCaller.cs | 52 +++++++ .../Features/OdsApi/OdsApiClient.cs | 97 ++++++++++++ .../OdsApi/OdsApiEndpointNameCount.cs | 15 ++ .../Helpers/AdminApiConnectioDataValidator.cs | 60 ++++++++ .../Helpers/Constants.cs | 22 +++ .../Helpers/InstanceValidator.cs | 43 ++++++ .../Helpers/JsonBuilder.cs | 28 ++++ .../Infrastructure/ApiResponse.cs | 26 ++++ .../Infrastructure/AppHttpClient.cs | 145 ++++++++++++++++++ .../HttpRequestMessageBuilder.cs | 40 +++++ .../Logging/DefaultLogger.cs | 22 +++ .../OdsApiSettings.cs | 16 ++ .../Program.cs | 74 +++++++++ .../Startup.cs | 89 +++++++++++ .../appsettings.json | 31 ++++ .../logging.json | 22 +++ .../Infrastructure/AdminApiEndpointBuilder.cs | 2 +- .../EdFi.Ods.AdminApi.csproj | 4 + .../Features/ClaimSets/ClaimSetModel.cs | 2 +- .../Features/Profiles/ProfileValidator.cs | 6 +- .../BackgroundJobs/HealthCheckJob.cs | 21 +++ .../HealthCheckServiceExtension.cs | 86 +++++++++++ .../Services/SimpleGetRequest.cs | 2 +- .../WebApplicationBuilderExtensions.cs | 24 +++ 38 files changed, 1528 insertions(+), 6 deletions(-) create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Application.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Program.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Startup.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/appsettings.json create mode 100644 Application/EdFi.AdminConsole.HealthCheckService/logging.json create mode 100644 Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs create mode 100644 Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index 975da9156..25db6951a 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.AdminConsole.UnitTests", "EdFi.Ods.AdminApi.AdminConsole.UnitTests\EdFi.Ods.AdminApi.AdminConsole.UnitTests.csproj", "{7C919128-B651-4756-8625-A8F7882FA9A4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.AdminConsole.HealthCheckService", "EdFi.AdminConsole.HealthCheckService\EdFi.AdminConsole.HealthCheckService.csproj", "{89457FDC-0611-4703-DA46-DEDD38C5FE22}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +104,14 @@ Global {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|Any CPU.Build.0 = Release|Any CPU {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|x64.ActiveCfg = Release|Any CPU {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|x64.Build.0 = Release|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|x64.ActiveCfg = Debug|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|x64.Build.0 = Debug|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|Any CPU.Build.0 = Release|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.ActiveCfg = Release|Any CPU + {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Application/Ed-Fi-ODS-AdminApi.sln.DotSettings b/Application/Ed-Fi-ODS-AdminApi.sln.DotSettings index 2bdd201f0..aa0b9430a 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln.DotSettings +++ b/Application/Ed-Fi-ODS-AdminApi.sln.DotSettings @@ -130,6 +130,10 @@ See the LICENSE and NOTICES files in the project root for more information.<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -188,6 +192,7 @@ See the LICENSE and NOTICES files in the project root for more information.True True True + True True True True diff --git a/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs b/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs new file mode 100644 index 000000000..cfcff84fd --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService; + +public interface IAdminApiSettings +{ + string AdminConsoleTenantsURL { get; set; } + string AdminConsoleInstancesURL { get; set; } + string AdminConsoleHealthCheckURL { get; set; } + string AccessTokenUrl { get; set; } + string Username { get; set; } + string ClientId { get; set; } + string ClientSecret { get; set; } + string Password { get; set; } + string GrantType { get; set; } + string Scope { get; set; } +} + +public sealed class AdminApiSettings : IAdminApiSettings +{ + public string AdminConsoleTenantsURL { get; set; } = string.Empty; + public string AdminConsoleInstancesURL { get; set; } = string.Empty; + public string AdminConsoleHealthCheckURL { get; set; } = string.Empty; + public string AccessTokenUrl { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string ClientId { get; set; } = string.Empty; + public string ClientSecret { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string GrantType { get; set; } = string.Empty; + public string Scope { get; set; } = string.Empty; +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs b/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs new file mode 100644 index 000000000..bbbd2a587 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService; + +public interface IAppSettings +{ + bool IgnoresCertificateErrors { get; set; } +} + +public sealed class AppSettings : IAppSettings +{ + public bool IgnoresCertificateErrors { get; set; } = false; + public int MaxRetryAttempts { get; set; } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs new file mode 100644 index 000000000..c6d4a5406 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService; + +public interface IApplication +{ + Task Run(); +} + +public class Application( + ILogger logger, + IAdminApiCaller adminApiCaller, + IOdsApiCaller odsApiCaller + ) : IApplication, IHostedService +{ + private readonly ILogger _logger = logger; + private readonly IAdminApiCaller _adminApiCaller = adminApiCaller; + private readonly IOdsApiCaller _odsApiCaller = odsApiCaller; + + public async Task Run() + { + /// Step 1. Get tenants data from Admin API - Admin Console extension. + _logger.LogInformation("Get tenants on Admin Api."); + var tenants = await _adminApiCaller.GetTenantsAsync(); + + if (!tenants.Any()) + _logger.LogInformation("No tenants returned from Admin Api."); + else + { + foreach (var tenantName in tenants.Select(tenant => tenant.Document.Name)) + { + /// Step 2. Get instances data from Admin API - Admin Console extension. + var instances = await _adminApiCaller.GetInstancesAsync(tenantName); + + if (instances == null || !instances.Any()) + { + _logger.LogInformation("No instances found on Admin Api."); + } + else + { + foreach (var instance in instances) + { + /// Step 3. For each instance, Get the HealthCheck data from ODS API + _logger.LogInformation("Processing instance with name: {InstanceName}", instance.InstanceName ?? ""); + + if (InstanceValidator.IsInstanceValid(_logger, instance)) + { + var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(instance); + + if (healthCheckData != null && healthCheckData.Count > 0) + { + _logger.LogInformation("HealCheck data obtained."); + + var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); + + /// Step 4. Post the HealthCheck data to the Admin API + var healthCheckPayload = new AdminApiHealthCheckPost() + { + TenantId = instance.TenantId, + InstanceId = instance.Id, + Document = healthCheckDocument.ToString(), + }; + + _logger.LogInformation("Posting HealthCheck data to Admin Api."); + + await _adminApiCaller.PostHealthCheckAsync(healthCheckPayload, tenantName); + } + else + { + _logger.LogInformation( + "No HealthCheck data has been collected for instance with name: {InstanceName}", instance.InstanceName + ); + } + } + } + } + } + + _logger.LogInformation("Process completed."); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await Run(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await Task.CompletedTask; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj new file mode 100644 index 000000000..b233255ca --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj @@ -0,0 +1,47 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + Always + + + Always + + + + diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec new file mode 100644 index 000000000..944097d97 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec @@ -0,0 +1,17 @@ +?xml version="1.0"?> + + + + 1.0.0 + Ed-Fi Admin Console Health Check Worker Process + Ed-Fi Alliance + https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-Admin-Console-Health-Check-Worker-Process + Copyright @ $year$ Ed-Fi Alliance, LLC and Contributors + Ed-Fi Admin Health Check Worker Process + Ed-Fi AdminConsoleHealthCheckWorkerProcess + Apache-2.0 + + + + + diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs b/Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs new file mode 100644 index 000000000..6cb2843dd --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; + +namespace EdFi.AdminConsole.HealthCheckService.Extensions; + +public static class HttpStatusCodeExtensions +{ + public static bool IsPotentiallyTransientFailure(this HttpStatusCode httpStatusCode) + { + switch (httpStatusCode) + { + case HttpStatusCode.InternalServerError: + case HttpStatusCode.GatewayTimeout: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.RequestTimeout: + return true; + default: + return false; + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs new file mode 100644 index 000000000..0f67b06a3 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; + +public interface IAdminApiCaller +{ + Task> GetTenantsAsync(); + Task> GetInstancesAsync(string? tenant); + Task PostHealthCheckAsync(AdminApiHealthCheckPost instanceHealthCheckData, string? tenant); +} + +public class AdminApiCaller(ILogger logger, IAdminApiClient adminApiClient, IOptions adminApiOptions) : IAdminApiCaller +{ + private readonly ILogger _logger = logger; + private readonly IAdminApiClient _adminApiClient = adminApiClient; + private readonly AdminApiSettings _adminApiOptions = adminApiOptions.Value; + + public async Task> GetTenantsAsync() + { + if (AdminApiConnectioDataValidator.IsValid(_logger, _adminApiOptions)) + { + var response = await _adminApiClient.AdminApiGet(_adminApiOptions.AdminConsoleTenantsURL, null); + var tenants = new List(); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content)) + { + var tenantsJObject = JsonConvert.DeserializeObject>(response.Content); + if (tenantsJObject != null) + { + foreach (var jObjectItem in tenantsJObject) + { + try + { + var jsonString = jObjectItem.ToString(); + if (jsonString.StartsWith("{{") && jsonString.EndsWith("}}")) + { + jsonString = jsonString[1..^1]; + } + var tenant = JsonConvert.DeserializeObject(jsonString); + if (tenant != null) + tenants.Add(tenant); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Not able to process tenant."); + } + } + } + } + return tenants; + } + else + { + _logger.LogError("AdminApi Settings has not been set properly."); + return []; + } + } + + public async Task> GetInstancesAsync(string? tenant) + { + if (AdminApiConnectioDataValidator.IsValid(_logger, _adminApiOptions)) + { + var response = await _adminApiClient.AdminApiGet(_adminApiOptions.AdminConsoleInstancesURL + Constants.CompletedInstances, tenant); + var instances = new List(); + + if (response.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content)) + { + var instancesJObject = JsonConvert.DeserializeObject>(response.Content); + if (instancesJObject != null) + { + foreach (var jObjectItem in instancesJObject) + { + try + { + var instance = JsonConvert.DeserializeObject(jObjectItem.ToString()); + if (instance != null) + instances.Add(instance); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Not able to process instance."); + } + } + } + } + return instances; + } + else + { + _logger.LogError("AdminApi Settings has not been set properly."); + return []; + } + } + + public async Task PostHealthCheckAsync(AdminApiHealthCheckPost instanceHealthCheckData, string? tenant) + { + var response = await _adminApiClient.AdminApiPost(_adminApiOptions.AdminConsoleHealthCheckURL, tenant, instanceHealthCheckData); + + if (response.StatusCode is not System.Net.HttpStatusCode.Created and not System.Net.HttpStatusCode.OK) + { + _logger.LogError("Not able to post HealthCheck data to Ods Api. Tenant Id: {TenantId}.", instanceHealthCheckData.TenantId); + _logger.LogError("Status Code returned is: {StatusCode}.", response.StatusCode); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs new file mode 100644 index 000000000..bfd12845c --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; + +public interface IAdminApiClient +{ + Task AdminApiGet(string url, string? tenant); + + Task AdminApiPost(string url, string? tenant, object? body = null); +} + +public class AdminApiClient( + IAppHttpClient appHttpClient, + ILogger logger, + IOptions adminApiOptions + ) : IAdminApiClient +{ + private readonly IAppHttpClient _appHttpClient = appHttpClient; + protected readonly ILogger _logger = logger; + private readonly AdminApiSettings _adminApiOptions = adminApiOptions.Value; + private string _accessToken = string.Empty; + + public async Task AdminApiGet(string url, string? tenant) + { + ApiResponse response = new ApiResponse(HttpStatusCode.InternalServerError, "Unknown error."); + await GetAccessToken(); + + if (!string.IsNullOrEmpty(_accessToken)) + { + StringContent? content = null; + if (!string.IsNullOrEmpty(tenant)) + { + content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); + content.Headers.Add("tenant", tenant); + } + + response = await _appHttpClient.SendAsync(url, + HttpMethod.Get, + content, + new AuthenticationHeaderValue("bearer", _accessToken) + ); + } + + return response; + } + + public async Task AdminApiPost(string url, string? tenant, object? body = null) + { + ApiResponse response = new ApiResponse(HttpStatusCode.InternalServerError, "Unknown error."); + await GetAccessToken(); + + if (!string.IsNullOrEmpty(_accessToken)) + { + StringContent? content = new StringContent(body != null ? JsonConvert.SerializeObject(body) : string.Empty, Encoding.UTF8, "application/json"); + + if (!string.IsNullOrEmpty(tenant)) + { + content.Headers.Add("tenant", tenant); + } + + response = await _appHttpClient.SendAsync( + url, + HttpMethod.Post, + content, + new AuthenticationHeaderValue("bearer", _accessToken) + ); + } + + return response; + } + + protected async Task GetAccessToken() + { + if (string.IsNullOrEmpty(_accessToken)) + { + FormUrlEncodedContent content = new FormUrlEncodedContent( + new List> + { + new("username", _adminApiOptions.Username), + new("client_id", _adminApiOptions.ClientId), + new("client_secret", _adminApiOptions.ClientSecret), + new("password", _adminApiOptions.Password), + new("grant_type", _adminApiOptions.GrantType), + new("scope", _adminApiOptions.Scope) + } + ); + + content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var apiResponse = await _appHttpClient.SendAsync( + _adminApiOptions.AccessTokenUrl, + HttpMethod.Post, + content, + null + ); + + if (apiResponse.StatusCode == HttpStatusCode.OK) + { + dynamic jsonToken = JToken.Parse(apiResponse.Content); + _accessToken = jsonToken["access_token"].ToString(); + } + else + { + _logger.LogError("Not able to get Admin Api Access Token. Status Code: {0}", apiResponse.StatusCode.ToString()); + } + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs new file mode 100644 index 000000000..2bc01ccc3 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; + +public class AdminApiHealthCheckPost +{ + public int DocId { get; set; } = 0; + public int InstanceId { get; set; } = 0; + public int EdOrgId { get; set; } = 0; + public int TenantId { get; set; } = 0; + public string Document { get; set; } = string.Empty; + +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs new file mode 100644 index 000000000..f84aa6cad --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; + +public class AdminConsoleInstance +{ + public int TenantId { get; set; } = 0; + + public string TenantName { get; set; } = string.Empty; + + public int Id { get; set; } = 0; + + public int OdsInstanceId { get; set; } = 0; + + public string InstanceName { get; set; } = string.Empty; + + public string ResourceUrl { get; set; } = string.Empty; + + public string OauthUrl { get; set; } = string.Empty; + + public string ClientId { get; set; } = string.Empty; + + public string ClientSecret { get; set; } = string.Empty; + + public string Status { get; set; } = string.Empty; +} + +public enum InstanceStatus +{ + Pending, + Completed, + InProgress, + Pending_Delete, + Deleted, + Delete_Failed, + Error +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs new file mode 100644 index 000000000..19728a8ec --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.Json.Serialization; + +namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; + +public class AdminConsoleTenant +{ + public int TenantId { get; set; } = 0; + [JsonPropertyName("document")] + public AdminConsoleTenantDocument Document { get; set; } = new AdminConsoleTenantDocument(); +} + + +public class AdminConsoleTenantDocument +{ + public string EdfiApiDiscoveryUrl { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs new file mode 100644 index 000000000..f4d789958 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.Extensions.Options; +using System.Collections; + +namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; + +public interface IAppSettingsOdsApiEndpoints : IEnumerable +{ + +} + +public class AppSettingsOdsApiEndpoints : IAppSettingsOdsApiEndpoints +{ + private readonly List endpoints; + + public AppSettingsOdsApiEndpoints(IOptions odsApiOptions) + { + endpoints = new List(); + endpoints.AddRange(odsApiOptions.Value.Endpoints); + } + + public IEnumerator GetEnumerator() + { + return endpoints.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs new file mode 100644 index 000000000..fb3675256 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; + +public interface IOdsApiCaller +{ + Task> GetHealthCheckDataAsync(AdminConsoleInstance instance); +} + +public class OdsApiCaller(IOdsApiClient odsApiClient, IAppSettingsOdsApiEndpoints appSettingsOdsApiEndpoints) : IOdsApiCaller +{ + private readonly IOdsApiClient _odsApiClient = odsApiClient; + private readonly IAppSettingsOdsApiEndpoints _appSettingsOdsApiEndpoints = appSettingsOdsApiEndpoints; + + public async Task> GetHealthCheckDataAsync(AdminConsoleInstance instance) + { + var tasks = new List(); + + foreach (var appSettingsOdsApiEndpoint in _appSettingsOdsApiEndpoints) + { + var odsResourceEndpointUrl = $"{instance.ResourceUrl}{Constants.EdFiUri}/{appSettingsOdsApiEndpoint}{Constants.OdsApiQueryParams}"; + + tasks.Add(await GetCountPerEndpointAsync( + appSettingsOdsApiEndpoint, instance.OauthUrl, instance.ClientId, instance.ClientSecret, odsResourceEndpointUrl)); + } + + return tasks; + } + + protected async Task GetCountPerEndpointAsync(string odsApiEndpoint, string authUrl, string clientId, string clientSecret, string odsEndpointUrl) + { + var result = new OdsApiEndpointNameCount() + { + OdsApiEndpointName = odsApiEndpoint, + }; + var response = await _odsApiClient.OdsApiGet(authUrl, clientId, clientSecret, odsEndpointUrl); + + if (response != null && response.StatusCode == System.Net.HttpStatusCode.OK && response.Headers != null && response.Headers.Contains(Constants.TotalCountHeader)) + result.OdsApiEndpointCount = int.Parse(response.Headers.GetValues(Constants.TotalCountHeader).First()); + else + result.AnyErrros = true; + + return result; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs new file mode 100644 index 000000000..bfabddfb1 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; + +namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; + +public interface IOdsApiClient +{ + Task OdsApiGet(string authenticationUrl, string clientId, string clientSecret, string odsEndpointUrl); +} + +public class OdsApiClient : IOdsApiClient +{ + private readonly IAppHttpClient _appHttpClient; + protected readonly ILogger _logger; + protected readonly AppSettings _options; + + private string _accessToken; + + public OdsApiClient( + IAppHttpClient appHttpClient, + ILogger logger, + IOptions options + ) + { + _appHttpClient = appHttpClient; + _logger = logger; + _options = options.Value; + _accessToken = string.Empty; + } + + public async Task OdsApiGet( + string authenticationUrl, + string clientId, + string clientSecret, + string odsEndpointUrl + ) + { + ApiResponse response = new ApiResponse(HttpStatusCode.InternalServerError, string.Empty); + await GetAccessToken(authenticationUrl, clientId, clientSecret); + + if (!string.IsNullOrEmpty(_accessToken)) + { + response = await _appHttpClient.SendAsync(odsEndpointUrl, + HttpMethod.Get, + null as StringContent, + new AuthenticationHeaderValue("bearer", _accessToken) + ); + } + + return response; + } + + protected async Task GetAccessToken(string accessTokenUrl, string clientId, string clientSecret) + { + if (string.IsNullOrEmpty(_accessToken)) + { + FormUrlEncodedContent content; + + content = new FormUrlEncodedContent( + new List> + { + new KeyValuePair("Grant_type", "client_credentials"), + } + ); + + var encodedKeySecret = Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"); + + var apiResponse = await _appHttpClient.SendAsync( + accessTokenUrl, + HttpMethod.Post, + content, + new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encodedKeySecret)) + ); + + if (apiResponse.StatusCode == HttpStatusCode.OK) + { + dynamic jsonToken = JToken.Parse(apiResponse.Content); + _accessToken = jsonToken["access_token"].ToString(); + } + else + { + _logger.LogError("Not able to get Ods Api Access Token"); + } + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs new file mode 100644 index 000000000..74f4d3017 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; + +public class OdsApiEndpointNameCount +{ + public string OdsApiEndpointName { get; set; } = string.Empty; + + public int OdsApiEndpointCount { get; set; } = 0; + + public bool AnyErrros { get; set; } = false; +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs new file mode 100644 index 000000000..24e8ffd0a --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService.Helpers; + +public static class AdminApiConnectioDataValidator +{ + public static bool IsValid(ILogger logger, IAdminApiSettings adminApiSettings) + { + var messages = new List(); + + if (string.IsNullOrEmpty(adminApiSettings.AccessTokenUrl)) + messages.Add("AccessTokenUrl is required."); + + if (string.IsNullOrEmpty(adminApiSettings.AdminConsoleInstancesURL)) + messages.Add("AdminConsoleInstancesURL is required."); + + if (string.IsNullOrEmpty(adminApiSettings.AdminConsoleHealthCheckURL)) + messages.Add("AdminConsoleHealthCheckURL is required."); + + if (string.IsNullOrEmpty(adminApiSettings.ClientId)) + messages.Add("ClientId is required."); + + if (string.IsNullOrEmpty(adminApiSettings.Scope)) + messages.Add("Scope is required."); + + if (string.IsNullOrEmpty(adminApiSettings.GrantType)) + messages.Add("GrantType is required."); + else + { + if (adminApiSettings.GrantType.Equals("client_credentials", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(adminApiSettings.ClientSecret)) + messages.Add("Client Secret is required."); + } + else if (adminApiSettings.GrantType.Equals("password", StringComparison.OrdinalIgnoreCase)) + { + + if (string.IsNullOrEmpty(adminApiSettings.Username)) + messages.Add("Username is required."); + + if (string.IsNullOrEmpty(adminApiSettings.Password)) + messages.Add("Password is required."); + } + } + + if (messages != null && messages.Count > 0) + { + string concatenatedMessages = string.Concat(messages); + logger.LogWarning("The AdminApiSettings section on the App Settings file has not been set properly. {Messages}", concatenatedMessages); + return false; + } + + return true; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs new file mode 100644 index 000000000..4c047b646 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService.Helpers +{ + public struct Constants + { + public const string OdsApiQueryParams = "?offset=0&limit=0&totalCount=true"; + + public const string TotalCountHeader = "total-count"; + + public const string TenantHeader = "tenant"; + + public const string CompletedInstances = "?status=Completed"; + + public const string EdFiUri = "ed-fi"; + + public const int RetryStartingDelayMilliseconds = 500; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs new file mode 100644 index 000000000..bd1a72610 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService.Helpers; + +public static class InstanceValidator +{ + public static bool IsInstanceValid(ILogger logger, AdminConsoleInstance instance) + { + var messages = new List(); + + if (instance == null) + messages.Add("instance cannot be empty."); + else + { + if (string.IsNullOrEmpty(instance.OauthUrl)) + messages.Add("AuthenticationUrl is required."); + + if (string.IsNullOrEmpty(instance.ResourceUrl)) + messages.Add("ResourceUrl is required."); + + if (string.IsNullOrEmpty(instance.ClientId)) + messages.Add("ClientId is required."); + + if (string.IsNullOrEmpty(instance.ClientSecret)) + messages.Add("ClientSecret is required."); + } + + if (messages != null && messages.Count > 0) + { + string concatenatedMessages = string.Concat(messages); + logger.LogWarning("The instance {Name} obtained from Admin API is not properly formed. {Messages}", instance?.InstanceName, concatenatedMessages); + return false; + } + + return true; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs new file mode 100644 index 000000000..091cab99b --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using System.Text.Json.Nodes; + +namespace EdFi.AdminConsole.HealthCheckService.Helpers; + +public static class JsonBuilder +{ + public static JsonObject BuildJsonObject(IEnumerable healthCheckData) + { + JsonObject healthCheckDocument = new(); + + if (healthCheckData != null) + { + healthCheckDocument.Add(new KeyValuePair("healthy", !healthCheckData.Any(r => r.AnyErrros))); + foreach (var countPerEndpoint in healthCheckData) + { + healthCheckDocument.Add(new KeyValuePair(countPerEndpoint.OdsApiEndpointName, countPerEndpoint.OdsApiEndpointCount)); + } + } + + return healthCheckDocument; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs new file mode 100644 index 000000000..91997fc47 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Net.Http.Headers; + +namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; +public class ApiResponse +{ + public HttpStatusCode StatusCode { get; } + public string Content { get; } + public HttpResponseHeaders? Headers { get; } + + public ApiResponse(HttpStatusCode statusCode, string content) + { + StatusCode = statusCode; + Content = content; + Headers = null; + } + public ApiResponse(HttpStatusCode statusCode, string content, HttpResponseHeaders headers) : this(statusCode, content) + { + Headers = headers; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs new file mode 100644 index 000000000..f233eabcb --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using EdFi.AdminConsole.HealthCheckService.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Polly; +using Polly.Contrib.WaitAndRetry; +using Serilog.Core; +using Constants = EdFi.AdminConsole.HealthCheckService.Helpers.Constants; + +namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; + +public interface IAppHttpClient +{ + Task SendAsync(string uriString, HttpMethod method, StringContent? content, AuthenticationHeaderValue? authenticationHeaderValue); + + Task SendAsync(string uriString, HttpMethod method, FormUrlEncodedContent content, AuthenticationHeaderValue? authenticationHeaderValue); +} + +public class AppHttpClient(HttpClient httpClient, ILogger logger, IOptions options) : IAppHttpClient +{ + private readonly HttpClient _httpClient = httpClient; + protected readonly ILogger _logger = logger; + protected readonly AppSettings _options = options.Value; + + public async Task SendAsync(string uriString, HttpMethod method, StringContent? content, AuthenticationHeaderValue? authenticationHeaderValue) + { + var delay = Backoff.ExponentialBackoff( + TimeSpan.FromMilliseconds(Constants.RetryStartingDelayMilliseconds), + _options.MaxRetryAttempts); + + int attempts = 0; + + var retryPolicy = Policy + .HandleResult(r => r.StatusCode.IsPotentiallyTransientFailure()) + .WaitAndRetryAsync( + delay, + (result, ts, retryAttempt, ctx) => + { + _logger.LogWarning("Retrying {HttpMethod} for resource '{UriString}'. Failed with status '{StatusCode}'. Retrying... (retry #{RetryAttempt} of {MaxRetryAttempts} with {TotalSeconds:N1}s delay)", + method, uriString, result.Result.StatusCode, retryAttempt, _options.MaxRetryAttempts, ts.TotalSeconds); + }); + + var response = await retryPolicy.ExecuteAsync( + async (ctx, ct) => + { + attempts++; + + if (attempts > 1) + { + _logger.LogDebug("{HttpMethod} for resource '{UriString}'. Attempt #{Attempts}.", + method, uriString, attempts); + } + + var requestMessage = new HttpRequestMessage(method, uriString) + { + Content = content + }; + + if (authenticationHeaderValue != null) + { + requestMessage.Headers.Authorization = authenticationHeaderValue; + } + + return await _httpClient.SendAsync(requestMessage, ct); + }, + [], + CancellationToken.None); + + string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (response.StatusCode != HttpStatusCode.OK) + { + var message = $"{method} request for '{uriString}' reference failed with status '{response.StatusCode}': {responseContent}"; + _logger.LogWarning(message); + } + + return new ApiResponse(response.StatusCode, responseContent, response.Headers); + } + + /// Access Token + public async Task SendAsync(string uriString, HttpMethod method, FormUrlEncodedContent content, AuthenticationHeaderValue? authenticationHeaderValue) + { + var delay = Backoff.ExponentialBackoff( + TimeSpan.FromMilliseconds(Constants.RetryStartingDelayMilliseconds), + _options.MaxRetryAttempts); + + int attempts = 0; + + var retryPolicy = Policy + .HandleResult(r => r.StatusCode.IsPotentiallyTransientFailure()) + .WaitAndRetryAsync( + delay, + (result, ts, retryAttempt, ctx) => + { + _logger.LogWarning("Retrying {HttpMethod} for resource '{UriString}'. Failed with status '{StatusCode}'. Retrying... (retry #{RetryAttempt} of {MaxRetryAttempts} with {TotalSeconds:N1}s delay)", + method, uriString, result.Result.StatusCode, retryAttempt, _options.MaxRetryAttempts, ts.TotalSeconds); + }); + + using var requestMessage = new HttpRequestMessage(method, uriString) + { + Content = content + }; + + if (authenticationHeaderValue != null) + { + _httpClient.DefaultRequestHeaders.Authorization = authenticationHeaderValue; + } + + var response = await retryPolicy.ExecuteAsync( + async (ctx, ct) => + { + attempts++; + + if (attempts > 1) + { + _logger.LogDebug("{HttpMethod} for resource '{UriString}'. Attempt #{Attempts}.", + method, uriString, attempts); + } + + var requestMessage = new HttpRequestMessage(method, uriString) + { + Content = content + }; + + if (authenticationHeaderValue != null) + { + requestMessage.Headers.Authorization = authenticationHeaderValue; + } + + return await _httpClient.SendAsync(requestMessage, ct); + }, + [], + CancellationToken.None); + + var responseContent = await response.Content.ReadAsStringAsync(); + return new ApiResponse(response.StatusCode, responseContent, response.Headers); + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs new file mode 100644 index 000000000..5d5fce4de --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; + +public interface IHttpRequestMessageBuilder +{ + HttpRequestMessage GetHttpRequestMessage(string uriString, HttpMethod method, StringContent? content); + + HttpRequestMessage GetHttpRequestMessage(string uriString, HttpMethod method, FormUrlEncodedContent? content); +} + +public class HttpRequestMessageBuilder : IHttpRequestMessageBuilder +{ + public HttpRequestMessage GetHttpRequestMessage(string uriString, HttpMethod method, StringContent? content) + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(uriString), + Method = method, + Content = content + }; + + return request; + } + + public HttpRequestMessage GetHttpRequestMessage(string uriString, HttpMethod method, FormUrlEncodedContent? content) + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(uriString), + Method = method, + Content = content + }; + + return request; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs b/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs new file mode 100644 index 000000000..a43d3b1ba --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace EdFi.AdminConsole.HealthCheckService.Logging; + +public static class DefaultLogger +{ + public static Logger Build() + { + return new LoggerConfiguration() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss,fff}] [{Level:u4}] [{SourceContext}] {Message} {Exception} {NewLine}") + .CreateLogger(); + } +} \ No newline at end of file diff --git a/Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs b/Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs new file mode 100644 index 000000000..d6f92c4c0 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.AdminConsole.HealthCheckService; + +public interface IOdsApiSettings +{ + IEnumerable Endpoints { get; set; } +} + +public class OdsApiSettings : IOdsApiSettings +{ + public IEnumerable Endpoints { get; set; } = new List(); +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Program.cs b/Application/EdFi.AdminConsole.HealthCheckService/Program.cs new file mode 100644 index 000000000..0f34e9f83 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Program.cs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Serilog; +using System.Reflection; + +namespace EdFi.AdminConsole.HealthCheckService; + +public static class Program +{ + public static async Task Main(string[] args) + { + Log.Logger = DefaultLogger.Build(); + + try + { + await Run(args); + } + catch (Exception exception) + { + Log.Fatal(exception, "Host terminated unexpectedly"); + } + finally + { + await Log.CloseAndFlushAsync(); + } + } + + private static async Task Run(string[] args) + { + Log.Information("Building host"); + var host = CreateHostBuilder(args); + host.ConfigureServices( + (context, services) => services.ConfigureTransformLoadServices(context.Configuration)); + + host.UseConsoleLifetime(); + + using var builtHost = host.Build(); + + using var cancellationTokenSource = new CancellationTokenSource(); + + var assembly = Assembly.GetExecutingAssembly(); + var informationalVersion = assembly + .GetCustomAttribute() + ?.InformationalVersion; + + Log.Information("{Name} {Version} Starting", assembly.GetName().Name, informationalVersion); + + Log.Information("Starting host"); + await builtHost.StartAsync(cancellationTokenSource.Token); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args).ConfigureAppConfiguration(ConfigureAppConfig).UseSerilog(); + + private static void ConfigureAppConfig(HostBuilderContext context, IConfigurationBuilder config) + { + var runPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); + var loggingConfigFile = Path.Combine(runPath ?? "./", "logging.json"); + var env = context.HostingEnvironment; + + config + .AddJsonFile(loggingConfigFile, optional: false) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + + config.AddEnvironmentVariables(prefix: "EdFi_AdminConsole_"); + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs b/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs new file mode 100644 index 000000000..cfe9fba00 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService; + +public static class Startup +{ + public static IServiceCollection ConfigureTransformLoadServices( + this IServiceCollection services, + IConfiguration configuration + ) + { + services.AddOptions(); + services.Configure(configuration.GetSection("AppSettings")); + services.Configure(configuration.GetSection("AdminApiSettings")); + services.Configure(configuration.GetSection("OdsApiSettings")); + +#pragma warning disable CS8603 // Possible null reference return. + services.AddSingleton(sp => sp.GetService>()); +#pragma warning restore CS8603 // Possible null reference return. + + services.AddSingleton(); + + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + + services + .AddHttpClient( + "AppHttpClient", + x => + { + x.Timeout = TimeSpan.FromSeconds(500); + } + ) + .ConfigurePrimaryHttpMessageHandler(() => + { + var handler = new HttpClientHandler(); + if ( + configuration?.GetSection("AppSettings")?["IgnoresCertificateErrors"]?.ToLower() == "true" + ) + { + return IgnoresCertificateErrorsHandler(); + } + return handler; + }); + + services.AddTransient(); + + return services; + } + + private static HttpClientHandler IgnoresCertificateErrorsHandler() + { + var handler = new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, +#pragma warning disable S4830 // Server certificates should be verified during SSL/TLS connections + ServerCertificateCustomValidationCallback = ( + httpRequestMessage, + cert, + cetChain, + policyErrors + ) => + { + return true; + } + }; +#pragma warning restore S4830 + + return handler; + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json b/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json new file mode 100644 index 000000000..a291b4358 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json @@ -0,0 +1,31 @@ +{ + "AppSettings": { + "IgnoresCertificateErrors": true, + "MaxRetryAttempts" : 5 + }, + "AdminApiSettings": { + "AdminConsoleTenantsURL": "https://localhost/adminapi/adminconsole/tenants", + "AdminConsoleInstancesURL": "https://localhost/adminapi/adminconsole/instances", + "AdminConsoleHealthCheckURL": "https://localhost/adminapi/adminconsole/healthcheck", + "AccessTokenUrl": "https://localhost/auth/realms/edfi-admin-console/protocol/openid-connect/token", + "Username": "", + "ClientId": "adminconsole-worker-client", + "ClientSecret": "7tpYh5eZtL0ct99cmfCXUY3q5o2KxUTU", + "GrantType": "client_credentials", + "Password": "", + "Scope": "edfi_admin_api/worker" + }, + "OdsApiSettings": { + "Endpoints": [ + "studentSpecialEducationProgramAssociations", + "studentDisciplineIncidentBehaviorAssociations", + "studentSchoolAssociations", + "studentSchoolAttendanceEvents", + "studentSectionAssociations", + "staffEducationOrganizationAssignmentAssociations", + "staffSectionAssociations", + "courseTranscripts", + "sections" + ] + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/logging.json b/Application/EdFi.AdminConsole.HealthCheckService/logging.json new file mode 100644 index 000000000..a03245d9c --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService/logging.json @@ -0,0 +1,22 @@ +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "WebOptimizer": "Warning" + } + }, + "Using": [ "Serilog.Sinks.Console" ], + "WriteTo": { + "ConsoleSink": { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss,fff}] [{LevelShortName}] [{SourceContext}] {Message} {Exception} {NewLine}" + } + } + } + + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs index ceb0386c9..745f32ad4 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiEndpointBuilder.cs @@ -145,7 +145,7 @@ public void BuildForVersions(string authorizationPolicy, params AdminApiVersions { builder.WithResponseCode(400, FeatureCommonConstants.BadRequestResponseDescription); } - + foreach (var action in _routeOptions) { action(builder); diff --git a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj index 944336b95..af3ba3b6a 100644 --- a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj +++ b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj @@ -39,6 +39,9 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -51,6 +54,7 @@ + diff --git a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs index 7b60e9dd1..f93c661f9 100644 --- a/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/ClaimSets/ClaimSetModel.cs @@ -104,5 +104,5 @@ public interface IResourceClaimOnClaimSetRequest { int ClaimSetId { get; } int ResourceClaimId { get; } - public List? ResourceClaimActions { get; } + List? ResourceClaimActions { get; } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs index 5d58ef0e3..b517b354c 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Profiles/ProfileValidator.cs @@ -37,11 +37,11 @@ void EventHandler(object? sender, ValidationEventArgs e) if (profile != null && !string.IsNullOrEmpty(name)) { var profileName = profile.GetAttribute("name"); - if(!profileName.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + if (!profileName.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { - context.AddFailure(propertyName, $"Profile name attribute value should match with {name}." ); + context.AddFailure(propertyName, $"Profile name attribute value should match with {name}."); } - } + } } catch (Exception ex) { diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs new file mode 100644 index 000000000..62b3c2940 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService; +using Quartz; + +namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; + +public class HealthCheckJob(IApplication application, ILogger logger) : IJob +{ + private readonly IApplication _application = application; + private readonly ILogger _logger = logger; + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogInformation("Running scheduled health check..."); + await _application.Run(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs new file mode 100644 index 000000000..04e9e2ea1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using EdFi.AdminConsole.HealthCheckService; + +namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; + +public static class HealthCheckServiceExtension +{ + public static IServiceCollection ConfigureHealthCheckServices( + this IServiceCollection services, + IConfiguration configuration + ) + { + services.AddOptions(); + services.Configure(configuration.GetSection("AppSettings")); + services.Configure(configuration.GetSection("AdminApiSettings")); + services.Configure(configuration.GetSection("OdsApiSettings")); + +#pragma warning disable CS8603 // Possible null reference return. + services.AddSingleton(sp => sp.GetService>()); +#pragma warning restore CS8603 // Possible null reference return. + + services.AddSingleton(); + + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + + services + .AddHttpClient( + "AppHttpClient", + x => + { + x.Timeout = TimeSpan.FromSeconds(500); + } + ) + .ConfigurePrimaryHttpMessageHandler(() => + { + var handler = new HttpClientHandler(); + if ( + configuration?.GetSection("AppSettings")?["IgnoresCertificateErrors"]?.ToLower() == "true" + ) + { + return IgnoresCertificateErrorsHandler(); + } + return handler; + }); + + services.AddTransient(); + + return services; + } + + private static HttpClientHandler IgnoresCertificateErrorsHandler() + { + var handler = new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, +#pragma warning disable S4830 // Server certificates should be verified during SSL/TLS connections + ServerCertificateCustomValidationCallback = ( + httpRequestMessage, + cert, + cetChain, + policyErrors + ) => + { + return true; + } + }; +#pragma warning restore S4830 + + return handler; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs index 53e04d051..ddaf2cb66 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/SimpleGetRequest.cs @@ -9,7 +9,7 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Services; public interface ISimpleGetRequest { - public Task DownloadString(string address); + Task DownloadString(string address); } public class SimpleGetRequest : ISimpleGetRequest diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index fdaa4999f..fea42131d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -30,6 +30,8 @@ using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Quartz; +using EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; namespace EdFi.Ods.AdminApi.Infrastructure; @@ -215,6 +217,28 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) webApplicationBuilder.Services.AddHttpClient(); webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); + + webApplicationBuilder.Services.ConfigureHealthCheckServices( + webApplicationBuilder.Configuration + ); + + // Quartz.NET back end service + webApplicationBuilder.Services.AddQuartz(q => + { + var jobKey = new JobKey("HealthCheckJob"); + q.AddJob(opts => opts.WithIdentity(jobKey)); + + // Create a trigger that fires every 10 minutes + q.AddTrigger(opts => opts + .ForJob(jobKey) + .WithIdentity("HealthCheckJob-trigger") + .WithSimpleSchedule(x => x + .WithInterval(TimeSpan.FromMinutes(10)) + .RepeatForever()) + ); + }); + + webApplicationBuilder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); } private static void EnableMultiTenancySupport(this WebApplicationBuilder webApplicationBuilder) From 0aa1c437414006b36a4753197b7cf93494b2a7fe Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Fri, 25 Jul 2025 15:54:49 -0500 Subject: [PATCH 02/14] Update health check service --- .../Application.cs | 98 +++++--- ...dFi.AdminConsole.HealthCheckService.csproj | 9 +- .../Helpers/InstanceValidator.cs | 12 +- .../appsettings.json | 31 --- .../HealthCheckServiceExtension.cs | 1 + .../EdFi.Ods.AdminApi/appsettings.json | 29 ++- Docker/dev.pgsql.Dockerfile | 3 + setup-local.ps1 | 215 ++++++++++++++++++ 8 files changed, 316 insertions(+), 82 deletions(-) delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/appsettings.json create mode 100644 setup-local.ps1 diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs index c6d4a5406..c5e27f1cd 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs @@ -8,6 +8,11 @@ using EdFi.AdminConsole.HealthCheckService.Helpers; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; +using AutoMapper; +using System.Dynamic; +using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; namespace EdFi.AdminConsole.HealthCheckService; @@ -16,30 +21,34 @@ public interface IApplication Task Run(); } -public class Application( - ILogger logger, - IAdminApiCaller adminApiCaller, - IOdsApiCaller odsApiCaller - ) : IApplication, IHostedService +public class Application(ILogger logger, + IMapper mapper, + IAdminConsoleTenantsService adminConsoleTenantsService, + IGetInstancesQuery getInstancesQuery, + IOdsApiCaller odsApiCaller) + : IApplication, IHostedService { private readonly ILogger _logger = logger; - private readonly IAdminApiCaller _adminApiCaller = adminApiCaller; + private readonly IMapper _mapper = mapper; + private readonly IAdminConsoleTenantsService _adminConsoleTenantsService = adminConsoleTenantsService; + private readonly IGetInstancesQuery _getInstancesQuery = getInstancesQuery; private readonly IOdsApiCaller _odsApiCaller = odsApiCaller; public async Task Run() { /// Step 1. Get tenants data from Admin API - Admin Console extension. + _logger.LogInformation("Starting HealthCheck Service..."); _logger.LogInformation("Get tenants on Admin Api."); - var tenants = await _adminApiCaller.GetTenantsAsync(); + var tenants = await _adminConsoleTenantsService.GetTenantsAsync(true); if (!tenants.Any()) _logger.LogInformation("No tenants returned from Admin Api."); else { - foreach (var tenantName in tenants.Select(tenant => tenant.Document.Name)) + foreach (var tenantName in tenants.Select(tenant => GetTenantName(tenant))) { /// Step 2. Get instances data from Admin API - Admin Console extension. - var instances = await _adminApiCaller.GetInstancesAsync(tenantName); + var instances = await _getInstancesQuery.Execute(tenantName); if (instances == null || !instances.Any()) { @@ -50,36 +59,40 @@ public async Task Run() foreach (var instance in instances) { /// Step 3. For each instance, Get the HealthCheck data from ODS API - _logger.LogInformation("Processing instance with name: {InstanceName}", instance.InstanceName ?? ""); + _logger.LogInformation( + "Processing instance with name: {InstanceName}", + instance.InstanceName ?? "" + ); if (InstanceValidator.IsInstanceValid(_logger, instance)) { - var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(instance); - - if (healthCheckData != null && healthCheckData.Count > 0) - { - _logger.LogInformation("HealCheck data obtained."); - - var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); - - /// Step 4. Post the HealthCheck data to the Admin API - var healthCheckPayload = new AdminApiHealthCheckPost() - { - TenantId = instance.TenantId, - InstanceId = instance.Id, - Document = healthCheckDocument.ToString(), - }; - - _logger.LogInformation("Posting HealthCheck data to Admin Api."); - - await _adminApiCaller.PostHealthCheckAsync(healthCheckPayload, tenantName); - } - else - { - _logger.LogInformation( - "No HealthCheck data has been collected for instance with name: {InstanceName}", instance.InstanceName - ); - } + //var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(instance); + + //if (healthCheckData != null && healthCheckData.Count > 0) + //{ + // _logger.LogInformation("HealCheck data obtained."); + + // var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); + + // /// Step 4. Post the HealthCheck data to the Admin API + // var healthCheckPayload = new AdminApiHealthCheckPost() + // { + // TenantId = instance.TenantId, + // InstanceId = instance.Id, + // Document = healthCheckDocument.ToString(), + // }; + + // _logger.LogInformation("Posting HealthCheck data to Admin Api."); + + // await _adminApiCaller.PostHealthCheckAsync(healthCheckPayload, tenantName); + //} + //else + //{ + // _logger.LogInformation( + // "No HealthCheck data has been collected for instance with name: {InstanceName}", + // instance.InstanceName + // ); + //} } } } @@ -87,6 +100,19 @@ public async Task Run() _logger.LogInformation("Process completed."); } + + static string GetTenantName(TenantModel tenant) + { + if (tenant.Document is ExpandoObject expandoObject) + { + var dict = expandoObject as IDictionary; + if (dict != null && dict.TryGetValue("name", out var nameValue) && nameValue is string name) + { + return name; + } + } + return string.Empty; + } } public async Task StartAsync(CancellationToken cancellationToken) diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj index b233255ca..4b88c0962 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj +++ b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj @@ -32,13 +32,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + + + + + - - Always - Always diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs index bd1a72610..5c6e1494f 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs @@ -3,14 +3,14 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; using Microsoft.Extensions.Logging; namespace EdFi.AdminConsole.HealthCheckService.Helpers; public static class InstanceValidator { - public static bool IsInstanceValid(ILogger logger, AdminConsoleInstance instance) + public static bool IsInstanceValid(ILogger logger, Instance instance) { var messages = new List(); @@ -18,17 +18,11 @@ public static bool IsInstanceValid(ILogger logger, AdminConsoleInstance instance messages.Add("instance cannot be empty."); else { - if (string.IsNullOrEmpty(instance.OauthUrl)) + if (string.IsNullOrEmpty(instance.OAuthUrl)) messages.Add("AuthenticationUrl is required."); if (string.IsNullOrEmpty(instance.ResourceUrl)) messages.Add("ResourceUrl is required."); - - if (string.IsNullOrEmpty(instance.ClientId)) - messages.Add("ClientId is required."); - - if (string.IsNullOrEmpty(instance.ClientSecret)) - messages.Add("ClientSecret is required."); } if (messages != null && messages.Count > 0) diff --git a/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json b/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json deleted file mode 100644 index a291b4358..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/appsettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "AppSettings": { - "IgnoresCertificateErrors": true, - "MaxRetryAttempts" : 5 - }, - "AdminApiSettings": { - "AdminConsoleTenantsURL": "https://localhost/adminapi/adminconsole/tenants", - "AdminConsoleInstancesURL": "https://localhost/adminapi/adminconsole/instances", - "AdminConsoleHealthCheckURL": "https://localhost/adminapi/adminconsole/healthcheck", - "AccessTokenUrl": "https://localhost/auth/realms/edfi-admin-console/protocol/openid-connect/token", - "Username": "", - "ClientId": "adminconsole-worker-client", - "ClientSecret": "7tpYh5eZtL0ct99cmfCXUY3q5o2KxUTU", - "GrantType": "client_credentials", - "Password": "", - "Scope": "edfi_admin_api/worker" - }, - "OdsApiSettings": { - "Endpoints": [ - "studentSpecialEducationProgramAssociations", - "studentDisciplineIncidentBehaviorAssociations", - "studentSchoolAssociations", - "studentSchoolAttendanceEvents", - "studentSectionAssociations", - "staffEducationOrganizationAssignmentAssociations", - "staffSectionAssociations", - "courseTranscripts", - "sections" - ] - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs index 04e9e2ea1..03a32746b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -27,6 +27,7 @@ IConfiguration configuration #pragma warning restore CS8603 // Possible null reference return. services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index c17dadb7d..607ddb169 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -11,6 +11,31 @@ "IgnoresCertificateErrors": false, "EnableApplicationResetEndpoint": true }, + "AdminApiSettings": { + "AdminConsoleTenantsURL": "https://localhost/adminapi/adminconsole/tenants", + "AdminConsoleInstancesURL": "https://localhost/adminapi/adminconsole/instances", + "AdminConsoleHealthCheckURL": "https://localhost/adminapi/adminconsole/healthcheck", + "AccessTokenUrl": "https://localhost/adminapi/connect/token", + "Username": "", + "ClientId": "adminconsole-worker-client", + "ClientSecret": "7tpYh5eZtL0ct99cmfCXUY3q5o2KxUTU", + "GrantType": "client_credentials", + "Password": "", + "Scope": "edfi_admin_api/worker" + }, + "OdsApiSettings": { + "Endpoints": [ + "studentSpecialEducationProgramAssociations", + "studentDisciplineIncidentBehaviorAssociations", + "studentSchoolAssociations", + "studentSchoolAttendanceEvents", + "studentSectionAssociations", + "staffEducationOrganizationAssignmentAssociations", + "staffSectionAssociations", + "courseTranscripts", + "sections" + ] + }, "AdminConsoleSettings": { "ApplicationName": "Ed-Fi Health Check", "ClaimsetName": "Ed-Fi ODS Admin Console", @@ -34,8 +59,8 @@ "AllowRegistration": true }, "SwaggerSettings": { - "EnableSwagger": false, - "DefaultTenant": "" + "EnableSwagger": true, + "DefaultTenant": "tenant1" }, "EnableDockerEnvironment": false, "ConnectionStrings": { diff --git a/Docker/dev.pgsql.Dockerfile b/Docker/dev.pgsql.Dockerfile index 34daa1f41..8a3efcbf4 100644 --- a/Docker/dev.pgsql.Dockerfile +++ b/Docker/dev.pgsql.Dockerfile @@ -24,6 +24,9 @@ COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminAp COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ +COPY --from=assets ./Application/NuGet.Config EdFi.AdminConsole.HealthCheckService/ +COPY --from=assets ./Application/EdFi.AdminConsole.HealthCheckService EdFi.AdminConsole.HealthCheckService/ + WORKDIR /source/EdFi.Ods.AdminApi RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT RUN dotnet restore && dotnet build -c Release diff --git a/setup-local.ps1 b/setup-local.ps1 new file mode 100644 index 000000000..ceee16678 --- /dev/null +++ b/setup-local.ps1 @@ -0,0 +1,215 @@ +<# +.SYNOPSIS + Sets up Ed-Fi AdminAPI multi-tenant Docker environment +.DESCRIPTION + This script runs the Docker Compose files to set up a multi-tenant Ed-Fi environment + with ODS databases and AdminAPI containers. +.PARAMETER EnvFile + Path to the environment file (default: .env) +.PARAMETER Down + Switch to bring down the containers instead of starting them +.PARAMETER Build + Switch to force rebuild of containers +.PARAMETER Logs + Switch to show logs after starting containers +#> + +param( + [string]$EnvFile = ".env", + [switch]$Down, + [switch]$Build, + [switch]$Logs +) + +# Set error action preference +$ErrorActionPreference = "Stop" + +# Define paths +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$ComposeDir = Join-Path $ScriptDir "Docker\Compose\pgsql\MultiTenant" +$OdsComposeFile = Join-Path $ComposeDir "compose-build-ods-multi-tenant.yml" +$DevComposeFile = Join-Path $ComposeDir "compose-build-dev-multi-tenant.yml" + +# Function to check if Docker is running +function Test-DockerRunning { + try { + docker info | Out-Null + return $true + } + catch { + return $false + } +} + +# Function to check if file exists +function Test-FileExists { + param([string]$FilePath, [string]$Description) + + if (-not (Test-Path $FilePath)) { + Write-Error "$Description not found at: $FilePath" + exit 1 + } + Write-Host "✓ Found $Description" -ForegroundColor Green +} + +# Function to run Docker Compose command with multiple files +function Invoke-MultiDockerCompose { + param( + [string[]]$ComposeFiles, + [string]$Command, + [string]$Description + ) + + Write-Host "$Description..." -ForegroundColor Cyan + + # Build the docker-compose command with multiple -f flags + $cmd = "docker-compose" + + # Add multiple -f flags for each compose file + foreach ($file in $ComposeFiles) { + $cmd += " -f `"$file`"" + Write-Host "Using compose file: $file" -ForegroundColor Gray + } + + # Add environment file if it exists + if (Test-Path $EnvFile) { + $cmd += " --env-file `"$EnvFile`"" + Write-Host "Using environment file: $EnvFile" -ForegroundColor Gray + } + + $cmd += " $Command" + + Write-Host "Executing: $cmd" -ForegroundColor Gray + + try { + Invoke-Expression $cmd + if ($LASTEXITCODE -eq 0) { + Write-Host "$Description completed successfully" -ForegroundColor Green + } else { + Write-Error "$Description failed with exit code: $LASTEXITCODE" + } + } + catch { + Write-Error "Failed to execute $Description`: $_" + exit 1 + } +} + +# Main execution +try { + Write-Host "=== Ed-Fi AdminAPI Multi-Tenant Docker Setup ===" -ForegroundColor Yellow + Write-Host "Timestamp: $(Get-Date)" -ForegroundColor Gray + + # Verify prerequisites + Write-Host "Checking prerequisites..." -ForegroundColor Cyan + + if (-not (Test-DockerRunning)) { + Write-Error "Docker is not running. Please start Docker Desktop and try again." + exit 1 + } + Write-Host "✓ Docker is running" -ForegroundColor Green + + # Check if docker-compose is available + try { + docker-compose --version | Out-Null + Write-Host "✓ Docker Compose is available" -ForegroundColor Green + } + catch { + Write-Error "Docker Compose is not available. Please install Docker Compose." + exit 1 + } + + # Verify compose files exist + Test-FileExists -FilePath $DevComposeFile -Description "Dev compose file" + Test-FileExists -FilePath $OdsComposeFile -Description "ODS compose file" + + # Check environment file + if (Test-Path $EnvFile) { + Write-Host "Using environment file: $EnvFile" -ForegroundColor Green + } else { + Write-Warning "Environment file not found: $EnvFile" + Write-Host "Continuing with default Docker environment variables..." -ForegroundColor Yellow + } + + # Define compose files array (order matters - Dev first, then ODS) + $ComposeFiles = @($DevComposeFile, $OdsComposeFile) + + if ($Down) { + # Bring down containers + Write-Host "Bringing down containers..." -ForegroundColor Red + + Invoke-MultiDockerCompose -ComposeFiles $ComposeFiles -Command "down --remove-orphans --volumes" -Description "Stopping all containers and removing volumes" + + Write-Host "All containers have been stopped and removed" -ForegroundColor Green + } + else { + # Start containers + Write-Host "Starting multi-tenant environment..." -ForegroundColor Green + + # Build command + $upCommand = "up -d" + if ($Build) { + $upCommand += " --build" + Write-Host "Building containers..." -ForegroundColor Yellow + } + + # Start all containers together + Invoke-MultiDockerCompose -ComposeFiles $ComposeFiles -Command $upCommand -Description "Starting multi-tenant containers" + + # Wait for all services to be ready + Write-Host "Waiting for all services to be ready..." -ForegroundColor Yellow + Start-Sleep -Seconds 20 + + # Show container status + Write-Host "Container Status:" -ForegroundColor Cyan + docker ps --filter "name=ed-fi" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + + # Check health of containers + Write-Host "`Health Check:" -ForegroundColor Cyan + $unhealthyContainers = docker ps --filter "name=ed-fi" --filter "health=unhealthy" --format "{{.Names}}" + if ($unhealthyContainers) { + Write-Warning "Some containers are unhealthy: $unhealthyContainers" + } else { + Write-Host "✓ All containers appear healthy" -ForegroundColor Green + } + + # Show logs if requested + if ($Logs) { + Write-Host "Container Logs:" -ForegroundColor Cyan + Write-Host "Press Ctrl+C to stop following logs..." -ForegroundColor Gray + + # Use the same compose files for logs + $logCmd = "docker-compose" + foreach ($file in $ComposeFiles) { + $logCmd += " -f `"$file`"" + } + if (Test-Path $EnvFile) { + $logCmd += " --env-file `"$EnvFile`"" + } + $logCmd += " logs -f" + + Invoke-Expression $logCmd + } + + Write-Host "Multi-tenant environment setup completed!" -ForegroundColor Green + Write-Host "Access points:" -ForegroundColor Cyan + Write-Host " • AdminAPI: http://localhost:5001" -ForegroundColor White + Write-Host " • Swagger: http://localhost:5001/swagger" -ForegroundColor White + Write-Host " • Health: http://localhost:5001/health" -ForegroundColor White + Write-Host "Useful commands:" -ForegroundColor Cyan + Write-Host " • View logs: .\setup-local.ps1 -Logs" -ForegroundColor White + Write-Host " • Stop all: .\setup-local.ps1 -Down" -ForegroundColor White + Write-Host " • Rebuild: .\setup-local.ps1 -Build" -ForegroundColor White + Write-Host " • Status: docker ps --filter name=ed-fi" -ForegroundColor White + } +} +catch { + Write-Error "Script execution failed: $_" + Write-Host "Troubleshooting tips:" -ForegroundColor Yellow + Write-Host " 1. Ensure Docker Desktop is running" -ForegroundColor White + Write-Host " 2. Check if ports 5001, 5432 are available" -ForegroundColor White + Write-Host " 3. Verify .env file contains required variables" -ForegroundColor White + Write-Host " 4. Try running with -Build flag to rebuild containers" -ForegroundColor White + Write-Host " 5. Check container logs: docker-compose -f Dev -f ODS logs" -ForegroundColor White + exit 1 +} From a90b19dce44030638fdbf8635262701640e36661 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Tue, 29 Jul 2025 11:23:07 -0500 Subject: [PATCH 03/14] Delete unwanted files --- .../AdminApiSettings.cs | 34 ----- .../Application.cs | 115 +++++++++++------ ...dFi.AdminConsole.HealthCheckService.csproj | 1 - .../Features/AdminApi/AdminApiCaller.cs | 115 ----------------- .../Features/AdminApi/AdminApiClient.cs | 121 ------------------ .../AdminApi/AdminApiHealthCheckPost.cs | 16 --- .../Features/AdminApi/AdminConsoleTenant.cs | 22 ---- .../Helpers/AdminApiConnectioDataValidator.cs | 60 --------- .../Helpers/InstanceValidator.cs | 12 +- .../Program.cs | 74 ----------- .../Startup.cs | 89 ------------- .../HealthCheckServiceExtension.cs | 14 +- .../WebApplicationBuilderExtensions.cs | 2 +- .../appsettings.Development.json | 6 +- .../EdFi.Ods.AdminApi/appsettings.json | 12 +- 15 files changed, 101 insertions(+), 592 deletions(-) delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Program.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Startup.cs diff --git a/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs b/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs deleted file mode 100644 index cfcff84fd..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/AdminApiSettings.cs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -namespace EdFi.AdminConsole.HealthCheckService; - -public interface IAdminApiSettings -{ - string AdminConsoleTenantsURL { get; set; } - string AdminConsoleInstancesURL { get; set; } - string AdminConsoleHealthCheckURL { get; set; } - string AccessTokenUrl { get; set; } - string Username { get; set; } - string ClientId { get; set; } - string ClientSecret { get; set; } - string Password { get; set; } - string GrantType { get; set; } - string Scope { get; set; } -} - -public sealed class AdminApiSettings : IAdminApiSettings -{ - public string AdminConsoleTenantsURL { get; set; } = string.Empty; - public string AdminConsoleInstancesURL { get; set; } = string.Empty; - public string AdminConsoleHealthCheckURL { get; set; } = string.Empty; - public string AccessTokenUrl { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; - public string ClientId { get; set; } = string.Empty; - public string ClientSecret { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public string GrantType { get; set; } = string.Empty; - public string Scope { get; set; } = string.Empty; -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs index c5e27f1cd..d344464b2 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs @@ -3,7 +3,6 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; using EdFi.AdminConsole.HealthCheckService.Helpers; using Microsoft.Extensions.Hosting; @@ -13,6 +12,12 @@ using AutoMapper; using System.Dynamic; using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Features.WorkerInstances; +using Newtonsoft.Json; +using System.Text; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; namespace EdFi.AdminConsole.HealthCheckService; @@ -22,17 +27,17 @@ public interface IApplication } public class Application(ILogger logger, - IMapper mapper, IAdminConsoleTenantsService adminConsoleTenantsService, IGetInstancesQuery getInstancesQuery, + IAddHealthCheckCommand addHealthCheckCommand, IOdsApiCaller odsApiCaller) : IApplication, IHostedService { private readonly ILogger _logger = logger; - private readonly IMapper _mapper = mapper; private readonly IAdminConsoleTenantsService _adminConsoleTenantsService = adminConsoleTenantsService; private readonly IGetInstancesQuery _getInstancesQuery = getInstancesQuery; private readonly IOdsApiCaller _odsApiCaller = odsApiCaller; + private readonly IAddHealthCheckCommand _addHealthCheckCommand = addHealthCheckCommand; public async Task Run() { @@ -45,10 +50,12 @@ public async Task Run() _logger.LogInformation("No tenants returned from Admin Api."); else { - foreach (var tenantName in tenants.Select(tenant => GetTenantName(tenant))) + foreach (var tenantName in tenants.Select(GetTenantName)) { + _logger.LogInformation("TenantName:{TenantName}", tenantName); + /// Step 2. Get instances data from Admin API - Admin Console extension. - var instances = await _getInstancesQuery.Execute(tenantName); + var instances = await _getInstancesQuery.Execute(tenantName, "Completed"); if (instances == null || !instances.Any()) { @@ -63,36 +70,34 @@ public async Task Run() "Processing instance with name: {InstanceName}", instance.InstanceName ?? "" ); - - if (InstanceValidator.IsInstanceValid(_logger, instance)) + var adminConsoleInstance = ConvertToAdminConsoleInstance(instance); + if (InstanceValidator.IsInstanceValid(_logger, adminConsoleInstance)) { - //var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(instance); - - //if (healthCheckData != null && healthCheckData.Count > 0) - //{ - // _logger.LogInformation("HealCheck data obtained."); - - // var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); - - // /// Step 4. Post the HealthCheck data to the Admin API - // var healthCheckPayload = new AdminApiHealthCheckPost() - // { - // TenantId = instance.TenantId, - // InstanceId = instance.Id, - // Document = healthCheckDocument.ToString(), - // }; - - // _logger.LogInformation("Posting HealthCheck data to Admin Api."); - - // await _adminApiCaller.PostHealthCheckAsync(healthCheckPayload, tenantName); - //} - //else - //{ - // _logger.LogInformation( - // "No HealthCheck data has been collected for instance with name: {InstanceName}", - // instance.InstanceName - // ); - //} + var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(adminConsoleInstance); + + if (healthCheckData != null && healthCheckData.Count > 0) + { + _logger.LogInformation("HealCheck data obtained."); + + var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); + + /// Step 4. Post the HealthCheck data to the Admin API + HealthCheckCommandModel healthCheckCommandModel = new( + instance.TenantId, + instance.Id, + healthCheckDocument.ToString() + ); + _logger.LogInformation("Posting HealthCheck data to Admin Api."); + + await _addHealthCheckCommand.Execute(healthCheckCommandModel); + } + else + { + _logger.LogInformation( + "No HealthCheck data has been collected for instance with name: {InstanceName}", + instance.InstanceName + ); + } } } } @@ -103,16 +108,37 @@ public async Task Run() static string GetTenantName(TenantModel tenant) { - if (tenant.Document is ExpandoObject expandoObject) + if (tenant.Document is ExpandoObject expandoObject && + expandoObject is IDictionary dict && + dict.TryGetValue("name", out var nameValue) && + nameValue is string name) { - var dict = expandoObject as IDictionary; - if (dict != null && dict.TryGetValue("name", out var nameValue) && nameValue is string name) - { - return name; - } + return name; } return string.Empty; } + + static AdminConsoleInstance ConvertToAdminConsoleInstance(Instance instance) + { + string? ClientId = null; + string? ClientSecret = null; + + if (instance.Credentials != null) + { + var credentials = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(instance.Credentials)); + ClientId = credentials?.ClientId; + ClientSecret = credentials?.Secret; + } + + return new AdminConsoleInstance + { + Id = instance.Id, + ResourceUrl = instance.ResourceUrl ?? string.Empty, + OauthUrl = instance.OAuthUrl ?? string.Empty, + ClientId = ClientId ?? string.Empty, + ClientSecret = ClientSecret ?? string.Empty + }; + } } public async Task StartAsync(CancellationToken cancellationToken) @@ -125,3 +151,12 @@ public async Task StopAsync(CancellationToken cancellationToken) await Task.CompletedTask; } } + +public class HealthCheckCommandModel(int tenantId, int instanceId, string document) : IAddHealthCheckModel +{ + public int TenantId { get; set; } = tenantId; + public int InstanceId { get; set; } = instanceId; + public string Document { get; set; } = document; + public int DocId => throw new NotImplementedException(); + public int EdOrgId => throw new NotImplementedException(); +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj index 4b88c0962..8c4fc49f7 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj +++ b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj @@ -1,7 +1,6 @@ - Exe net8.0 enable enable diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs deleted file mode 100644 index 0f67b06a3..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiCaller.cs +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Text; -using EdFi.AdminConsole.HealthCheckService.Helpers; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; - -public interface IAdminApiCaller -{ - Task> GetTenantsAsync(); - Task> GetInstancesAsync(string? tenant); - Task PostHealthCheckAsync(AdminApiHealthCheckPost instanceHealthCheckData, string? tenant); -} - -public class AdminApiCaller(ILogger logger, IAdminApiClient adminApiClient, IOptions adminApiOptions) : IAdminApiCaller -{ - private readonly ILogger _logger = logger; - private readonly IAdminApiClient _adminApiClient = adminApiClient; - private readonly AdminApiSettings _adminApiOptions = adminApiOptions.Value; - - public async Task> GetTenantsAsync() - { - if (AdminApiConnectioDataValidator.IsValid(_logger, _adminApiOptions)) - { - var response = await _adminApiClient.AdminApiGet(_adminApiOptions.AdminConsoleTenantsURL, null); - var tenants = new List(); - - if (response.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content)) - { - var tenantsJObject = JsonConvert.DeserializeObject>(response.Content); - if (tenantsJObject != null) - { - foreach (var jObjectItem in tenantsJObject) - { - try - { - var jsonString = jObjectItem.ToString(); - if (jsonString.StartsWith("{{") && jsonString.EndsWith("}}")) - { - jsonString = jsonString[1..^1]; - } - var tenant = JsonConvert.DeserializeObject(jsonString); - if (tenant != null) - tenants.Add(tenant); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Not able to process tenant."); - } - } - } - } - return tenants; - } - else - { - _logger.LogError("AdminApi Settings has not been set properly."); - return []; - } - } - - public async Task> GetInstancesAsync(string? tenant) - { - if (AdminApiConnectioDataValidator.IsValid(_logger, _adminApiOptions)) - { - var response = await _adminApiClient.AdminApiGet(_adminApiOptions.AdminConsoleInstancesURL + Constants.CompletedInstances, tenant); - var instances = new List(); - - if (response.StatusCode == System.Net.HttpStatusCode.OK && !string.IsNullOrEmpty(response.Content)) - { - var instancesJObject = JsonConvert.DeserializeObject>(response.Content); - if (instancesJObject != null) - { - foreach (var jObjectItem in instancesJObject) - { - try - { - var instance = JsonConvert.DeserializeObject(jObjectItem.ToString()); - if (instance != null) - instances.Add(instance); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Not able to process instance."); - } - } - } - } - return instances; - } - else - { - _logger.LogError("AdminApi Settings has not been set properly."); - return []; - } - } - - public async Task PostHealthCheckAsync(AdminApiHealthCheckPost instanceHealthCheckData, string? tenant) - { - var response = await _adminApiClient.AdminApiPost(_adminApiOptions.AdminConsoleHealthCheckURL, tenant, instanceHealthCheckData); - - if (response.StatusCode is not System.Net.HttpStatusCode.Created and not System.Net.HttpStatusCode.OK) - { - _logger.LogError("Not able to post HealthCheck data to Ods Api. Tenant Id: {TenantId}.", instanceHealthCheckData.TenantId); - _logger.LogError("Status Code returned is: {StatusCode}.", response.StatusCode); - } - } -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs deleted file mode 100644 index bfd12845c..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiClient.cs +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; - -public interface IAdminApiClient -{ - Task AdminApiGet(string url, string? tenant); - - Task AdminApiPost(string url, string? tenant, object? body = null); -} - -public class AdminApiClient( - IAppHttpClient appHttpClient, - ILogger logger, - IOptions adminApiOptions - ) : IAdminApiClient -{ - private readonly IAppHttpClient _appHttpClient = appHttpClient; - protected readonly ILogger _logger = logger; - private readonly AdminApiSettings _adminApiOptions = adminApiOptions.Value; - private string _accessToken = string.Empty; - - public async Task AdminApiGet(string url, string? tenant) - { - ApiResponse response = new ApiResponse(HttpStatusCode.InternalServerError, "Unknown error."); - await GetAccessToken(); - - if (!string.IsNullOrEmpty(_accessToken)) - { - StringContent? content = null; - if (!string.IsNullOrEmpty(tenant)) - { - content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); - content.Headers.Add("tenant", tenant); - } - - response = await _appHttpClient.SendAsync(url, - HttpMethod.Get, - content, - new AuthenticationHeaderValue("bearer", _accessToken) - ); - } - - return response; - } - - public async Task AdminApiPost(string url, string? tenant, object? body = null) - { - ApiResponse response = new ApiResponse(HttpStatusCode.InternalServerError, "Unknown error."); - await GetAccessToken(); - - if (!string.IsNullOrEmpty(_accessToken)) - { - StringContent? content = new StringContent(body != null ? JsonConvert.SerializeObject(body) : string.Empty, Encoding.UTF8, "application/json"); - - if (!string.IsNullOrEmpty(tenant)) - { - content.Headers.Add("tenant", tenant); - } - - response = await _appHttpClient.SendAsync( - url, - HttpMethod.Post, - content, - new AuthenticationHeaderValue("bearer", _accessToken) - ); - } - - return response; - } - - protected async Task GetAccessToken() - { - if (string.IsNullOrEmpty(_accessToken)) - { - FormUrlEncodedContent content = new FormUrlEncodedContent( - new List> - { - new("username", _adminApiOptions.Username), - new("client_id", _adminApiOptions.ClientId), - new("client_secret", _adminApiOptions.ClientSecret), - new("password", _adminApiOptions.Password), - new("grant_type", _adminApiOptions.GrantType), - new("scope", _adminApiOptions.Scope) - } - ); - - content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - - var apiResponse = await _appHttpClient.SendAsync( - _adminApiOptions.AccessTokenUrl, - HttpMethod.Post, - content, - null - ); - - if (apiResponse.StatusCode == HttpStatusCode.OK) - { - dynamic jsonToken = JToken.Parse(apiResponse.Content); - _accessToken = jsonToken["access_token"].ToString(); - } - else - { - _logger.LogError("Not able to get Admin Api Access Token. Status Code: {0}", apiResponse.StatusCode.ToString()); - } - } - } -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs deleted file mode 100644 index 2bc01ccc3..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminApiHealthCheckPost.cs +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; - -public class AdminApiHealthCheckPost -{ - public int DocId { get; set; } = 0; - public int InstanceId { get; set; } = 0; - public int EdOrgId { get; set; } = 0; - public int TenantId { get; set; } = 0; - public string Document { get; set; } = string.Empty; - -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs deleted file mode 100644 index 19728a8ec..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleTenant.cs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Text.Json.Serialization; - -namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; - -public class AdminConsoleTenant -{ - public int TenantId { get; set; } = 0; - [JsonPropertyName("document")] - public AdminConsoleTenantDocument Document { get; set; } = new AdminConsoleTenantDocument(); -} - - -public class AdminConsoleTenantDocument -{ - public string EdfiApiDiscoveryUrl { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs deleted file mode 100644 index 24e8ffd0a..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/AdminApiConnectioDataValidator.cs +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using Microsoft.Extensions.Logging; - -namespace EdFi.AdminConsole.HealthCheckService.Helpers; - -public static class AdminApiConnectioDataValidator -{ - public static bool IsValid(ILogger logger, IAdminApiSettings adminApiSettings) - { - var messages = new List(); - - if (string.IsNullOrEmpty(adminApiSettings.AccessTokenUrl)) - messages.Add("AccessTokenUrl is required."); - - if (string.IsNullOrEmpty(adminApiSettings.AdminConsoleInstancesURL)) - messages.Add("AdminConsoleInstancesURL is required."); - - if (string.IsNullOrEmpty(adminApiSettings.AdminConsoleHealthCheckURL)) - messages.Add("AdminConsoleHealthCheckURL is required."); - - if (string.IsNullOrEmpty(adminApiSettings.ClientId)) - messages.Add("ClientId is required."); - - if (string.IsNullOrEmpty(adminApiSettings.Scope)) - messages.Add("Scope is required."); - - if (string.IsNullOrEmpty(adminApiSettings.GrantType)) - messages.Add("GrantType is required."); - else - { - if (adminApiSettings.GrantType.Equals("client_credentials", StringComparison.OrdinalIgnoreCase)) - { - if (string.IsNullOrEmpty(adminApiSettings.ClientSecret)) - messages.Add("Client Secret is required."); - } - else if (adminApiSettings.GrantType.Equals("password", StringComparison.OrdinalIgnoreCase)) - { - - if (string.IsNullOrEmpty(adminApiSettings.Username)) - messages.Add("Username is required."); - - if (string.IsNullOrEmpty(adminApiSettings.Password)) - messages.Add("Password is required."); - } - } - - if (messages != null && messages.Count > 0) - { - string concatenatedMessages = string.Concat(messages); - logger.LogWarning("The AdminApiSettings section on the App Settings file has not been set properly. {Messages}", concatenatedMessages); - return false; - } - - return true; - } -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs index 5c6e1494f..bd1a72610 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs @@ -3,14 +3,14 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; using Microsoft.Extensions.Logging; namespace EdFi.AdminConsole.HealthCheckService.Helpers; public static class InstanceValidator { - public static bool IsInstanceValid(ILogger logger, Instance instance) + public static bool IsInstanceValid(ILogger logger, AdminConsoleInstance instance) { var messages = new List(); @@ -18,11 +18,17 @@ public static bool IsInstanceValid(ILogger logger, Instance instance) messages.Add("instance cannot be empty."); else { - if (string.IsNullOrEmpty(instance.OAuthUrl)) + if (string.IsNullOrEmpty(instance.OauthUrl)) messages.Add("AuthenticationUrl is required."); if (string.IsNullOrEmpty(instance.ResourceUrl)) messages.Add("ResourceUrl is required."); + + if (string.IsNullOrEmpty(instance.ClientId)) + messages.Add("ClientId is required."); + + if (string.IsNullOrEmpty(instance.ClientSecret)) + messages.Add("ClientSecret is required."); } if (messages != null && messages.Count > 0) diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Program.cs b/Application/EdFi.AdminConsole.HealthCheckService/Program.cs deleted file mode 100644 index 0f34e9f83..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Program.cs +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.AdminConsole.HealthCheckService.Logging; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Serilog; -using System.Reflection; - -namespace EdFi.AdminConsole.HealthCheckService; - -public static class Program -{ - public static async Task Main(string[] args) - { - Log.Logger = DefaultLogger.Build(); - - try - { - await Run(args); - } - catch (Exception exception) - { - Log.Fatal(exception, "Host terminated unexpectedly"); - } - finally - { - await Log.CloseAndFlushAsync(); - } - } - - private static async Task Run(string[] args) - { - Log.Information("Building host"); - var host = CreateHostBuilder(args); - host.ConfigureServices( - (context, services) => services.ConfigureTransformLoadServices(context.Configuration)); - - host.UseConsoleLifetime(); - - using var builtHost = host.Build(); - - using var cancellationTokenSource = new CancellationTokenSource(); - - var assembly = Assembly.GetExecutingAssembly(); - var informationalVersion = assembly - .GetCustomAttribute() - ?.InformationalVersion; - - Log.Information("{Name} {Version} Starting", assembly.GetName().Name, informationalVersion); - - Log.Information("Starting host"); - await builtHost.StartAsync(cancellationTokenSource.Token); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args).ConfigureAppConfiguration(ConfigureAppConfig).UseSerilog(); - - private static void ConfigureAppConfig(HostBuilderContext context, IConfigurationBuilder config) - { - var runPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); - var loggingConfigFile = Path.Combine(runPath ?? "./", "logging.json"); - var env = context.HostingEnvironment; - - config - .AddJsonFile(loggingConfigFile, optional: false) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - - config.AddEnvironmentVariables(prefix: "EdFi_AdminConsole_"); - } -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs b/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs deleted file mode 100644 index cfe9fba00..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Startup.cs +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace EdFi.AdminConsole.HealthCheckService; - -public static class Startup -{ - public static IServiceCollection ConfigureTransformLoadServices( - this IServiceCollection services, - IConfiguration configuration - ) - { - services.AddOptions(); - services.Configure(configuration.GetSection("AppSettings")); - services.Configure(configuration.GetSection("AdminApiSettings")); - services.Configure(configuration.GetSection("OdsApiSettings")); - -#pragma warning disable CS8603 // Possible null reference return. - services.AddSingleton(sp => sp.GetService>()); -#pragma warning restore CS8603 // Possible null reference return. - - services.AddSingleton(); - - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - - services - .AddHttpClient( - "AppHttpClient", - x => - { - x.Timeout = TimeSpan.FromSeconds(500); - } - ) - .ConfigurePrimaryHttpMessageHandler(() => - { - var handler = new HttpClientHandler(); - if ( - configuration?.GetSection("AppSettings")?["IgnoresCertificateErrors"]?.ToLower() == "true" - ) - { - return IgnoresCertificateErrorsHandler(); - } - return handler; - }); - - services.AddTransient(); - - return services; - } - - private static HttpClientHandler IgnoresCertificateErrorsHandler() - { - var handler = new HttpClientHandler - { - ClientCertificateOptions = ClientCertificateOption.Manual, -#pragma warning disable S4830 // Server certificates should be verified during SSL/TLS connections - ServerCertificateCustomValidationCallback = ( - httpRequestMessage, - cert, - cetChain, - policyErrors - ) => - { - return true; - } - }; -#pragma warning restore S4830 - - return handler; - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs index 03a32746b..b5be2f563 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -7,6 +7,9 @@ using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; using EdFi.AdminConsole.HealthCheckService.Infrastructure; using EdFi.AdminConsole.HealthCheckService; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; @@ -19,7 +22,6 @@ IConfiguration configuration { services.AddOptions(); services.Configure(configuration.GetSection("AppSettings")); - services.Configure(configuration.GetSection("AdminApiSettings")); services.Configure(configuration.GetSection("OdsApiSettings")); #pragma warning disable CS8603 // Possible null reference return. @@ -27,14 +29,14 @@ IConfiguration configuration #pragma warning restore CS8603 // Possible null reference return. services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - services.AddTransient(); services.AddTransient(); - - services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -59,8 +61,6 @@ IConfiguration configuration return handler; }); - services.AddTransient(); - return services; } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index fea42131d..965eb048e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -233,7 +233,7 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) .ForJob(jobKey) .WithIdentity("HealthCheckJob-trigger") .WithSimpleSchedule(x => x - .WithInterval(TimeSpan.FromMinutes(10)) + .WithInterval(TimeSpan.FromMinutes(2)) .RepeatForever()) ); }); diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Development.json b/Application/EdFi.Ods.AdminApi/appsettings.Development.json index 728ebf299..5e956238a 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.Development.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.Development.json @@ -2,7 +2,7 @@ "AppSettings": { "MultiTenancy": false, "EnableAdminConsoleAPI": true, - "DatabaseEngine": "SqlServer", + "DatabaseEngine": "PostgreSql", "IgnoresCertificateErrors": true }, "AdminConsoleSettings": { @@ -22,8 +22,8 @@ "AllowRegistration": true }, "ConnectionStrings": { - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True;Encrypt=false", - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True;Encrypt=false" + "EdFi_Admin": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Admin;pooling=false", + "EdFi_Security": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Security;pooling=false" }, "SwaggerSettings": { "EnableSwagger": true, diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 607ddb169..07b4a09b0 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -64,8 +64,8 @@ }, "EnableDockerEnvironment": false, "ConnectionStrings": { - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True", - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True" + "EdFi_Admin": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Admin;pooling=false", + "EdFi_Security": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Security;pooling=false" }, "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api/", "Log4NetCore": { @@ -82,15 +82,15 @@ "Tenants": { "tenant1": { "ConnectionStrings": { - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True", - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True" + "EdFi_Admin": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Admin;pooling=false", + "EdFi_Security": "host=localhost;port=5401;username=postgres;password=postgres;database=EdFi_Security;pooling=false" }, "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api6/" }, "tenant2": { "ConnectionStrings": { - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True", - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True" + "EdFi_Admin": "host=localhost;port=5402;username=postgres;password=postgres;database=EdFi_Admin;pooling=false", + "EdFi_Security": "host=localhost;port=5402;username=postgres;password=postgres;database=EdFi_Security;pooling=false" }, "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api4/" } From 75cb247c9959fac4f9500339f9e3c450cfd4a3ce Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Tue, 29 Jul 2025 18:03:51 -0500 Subject: [PATCH 04/14] Update db ports --- .../EdFi.AdminConsole.HealthCheckService/Application.cs | 4 ++-- .../pgsql/MultiTenant/compose-build-ods-multi-tenant.yml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs index d344464b2..f02eafc67 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Application.cs @@ -157,6 +157,6 @@ public class HealthCheckCommandModel(int tenantId, int instanceId, string docume public int TenantId { get; set; } = tenantId; public int InstanceId { get; set; } = instanceId; public string Document { get; set; } = document; - public int DocId => throw new NotImplementedException(); - public int EdOrgId => throw new NotImplementedException(); + public int DocId { get; set; } + public int EdOrgId { get; set; } } diff --git a/Docker/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml b/Docker/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml index 4c35197b5..0ec9f6063 100644 --- a/Docker/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml +++ b/Docker/Compose/pgsql/MultiTenant/compose-build-ods-multi-tenant.yml @@ -85,6 +85,8 @@ services: PGBOUNCER_SET_DATABASE_USER: "yes" PGBOUNCER_SET_DATABASE_PASSWORD: "yes" restart: always + ports: + - "6401:6432" container_name: ed-fi-pb-ods-tenant1 depends_on: - db-ods-tenant1 @@ -102,6 +104,8 @@ services: PGBOUNCER_SET_DATABASE_PASSWORD: "yes" restart: always container_name: ed-fi-pb-ods-tenant2 + ports: + - "6402:6432" depends_on: - db-ods-tenant2 From 36ba11a34ada522ef95e09feab7322ac0f1113d7 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Wed, 30 Jul 2025 18:12:37 -0500 Subject: [PATCH 05/14] Refactor the health check service files --- .../Features/OdsApi/OdsApiCaller.cs | 1 - .../Features/OdsApi/OdsApiClient.cs | 5 +- .../{Application.cs => HealthCheckService.cs} | 127 +++++++++--------- .../Infrastructure/AppHttpClient.cs | 6 +- .../Instances/Models/IInstanceRequestModel.cs | 2 +- .../Services/Tenants/TenantService.cs | 6 +- .../BackgroundJobs/HealthCheckJob.cs | 6 +- .../HealthCheckServiceExtension.cs | 43 +++--- .../WebApplicationBuilderExtensions.cs | 43 +++--- Application/EdFi.Ods.AdminApi/Program.cs | 4 + .../EdFi.Ods.AdminConsole.DBTests/Testing.cs | 6 +- 11 files changed, 117 insertions(+), 132 deletions(-) rename Application/EdFi.AdminConsole.HealthCheckService/{Application.cs => HealthCheckService.cs} (50%) diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs index fb3675256..fb8b74737 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs @@ -5,7 +5,6 @@ using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; using EdFi.AdminConsole.HealthCheckService.Helpers; -using Microsoft.Extensions.Logging; namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs index bfabddfb1..83cf149c7 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs @@ -3,7 +3,6 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; using System.Net; using System.Net.Http.Headers; using System.Text; @@ -22,14 +21,14 @@ public interface IOdsApiClient public class OdsApiClient : IOdsApiClient { private readonly IAppHttpClient _appHttpClient; - protected readonly ILogger _logger; + protected readonly ILogger _logger; protected readonly AppSettings _options; private string _accessToken; public OdsApiClient( IAppHttpClient appHttpClient, - ILogger logger, + ILogger logger, IOptions options ) { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs similarity index 50% rename from Application/EdFi.AdminConsole.HealthCheckService/Application.cs rename to Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs index f02eafc67..3c2841a27 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Application.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs @@ -5,11 +5,9 @@ using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; using EdFi.AdminConsole.HealthCheckService.Helpers; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; -using AutoMapper; using System.Dynamic; using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; @@ -21,17 +19,17 @@ namespace EdFi.AdminConsole.HealthCheckService; -public interface IApplication +public interface IHealthCheckService { Task Run(); } -public class Application(ILogger logger, +public class HealthCheckService(ILogger logger, IAdminConsoleTenantsService adminConsoleTenantsService, IGetInstancesQuery getInstancesQuery, IAddHealthCheckCommand addHealthCheckCommand, IOdsApiCaller odsApiCaller) - : IApplication, IHostedService + : IHealthCheckService { private readonly ILogger _logger = logger; private readonly IAdminConsoleTenantsService _adminConsoleTenantsService = adminConsoleTenantsService; @@ -41,69 +39,76 @@ public class Application(ILogger logger, public async Task Run() { - /// Step 1. Get tenants data from Admin API - Admin Console extension. - _logger.LogInformation("Starting HealthCheck Service..."); - _logger.LogInformation("Get tenants on Admin Api."); - var tenants = await _adminConsoleTenantsService.GetTenantsAsync(true); - - if (!tenants.Any()) - _logger.LogInformation("No tenants returned from Admin Api."); - else + try { - foreach (var tenantName in tenants.Select(GetTenantName)) + /// Step 1. Get tenants data from Admin API - Admin Console extension. + _logger.LogInformation("Starting HealthCheck Service..."); + _logger.LogInformation("Get tenants on Admin Api."); + var tenants = await _adminConsoleTenantsService.GetTenantsAsync(true); + + if (tenants.Count == 0) + _logger.LogInformation("No tenants returned from Admin Api."); + else { - _logger.LogInformation("TenantName:{TenantName}", tenantName); + foreach (var tenantName in tenants.Select(GetTenantName)) + { + _logger.LogInformation("TenantName:{TenantName}", tenantName); - /// Step 2. Get instances data from Admin API - Admin Console extension. - var instances = await _getInstancesQuery.Execute(tenantName, "Completed"); + /// Step 2. Get instances data from Admin API - Admin Console extension. + var instances = await _getInstancesQuery.Execute(tenantName, "Completed"); - if (instances == null || !instances.Any()) - { - _logger.LogInformation("No instances found on Admin Api."); - } - else - { - foreach (var instance in instances) + if (instances == null || !instances.Any()) + { + _logger.LogInformation("No instances found on Admin Api."); + } + else { - /// Step 3. For each instance, Get the HealthCheck data from ODS API - _logger.LogInformation( - "Processing instance with name: {InstanceName}", - instance.InstanceName ?? "" - ); - var adminConsoleInstance = ConvertToAdminConsoleInstance(instance); - if (InstanceValidator.IsInstanceValid(_logger, adminConsoleInstance)) + foreach (var instance in instances) { - var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(adminConsoleInstance); - - if (healthCheckData != null && healthCheckData.Count > 0) - { - _logger.LogInformation("HealCheck data obtained."); - - var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); - - /// Step 4. Post the HealthCheck data to the Admin API - HealthCheckCommandModel healthCheckCommandModel = new( - instance.TenantId, - instance.Id, - healthCheckDocument.ToString() - ); - _logger.LogInformation("Posting HealthCheck data to Admin Api."); - - await _addHealthCheckCommand.Execute(healthCheckCommandModel); - } - else + /// Step 3. For each instance, Get the HealthCheck data from ODS API + _logger.LogInformation( + "Processing instance with name: {InstanceName}", + instance.InstanceName ?? "" + ); + var adminConsoleInstance = ConvertToAdminConsoleInstance(instance); + if (InstanceValidator.IsInstanceValid(_logger, adminConsoleInstance)) { - _logger.LogInformation( - "No HealthCheck data has been collected for instance with name: {InstanceName}", - instance.InstanceName - ); + var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(adminConsoleInstance); + + if (healthCheckData != null && healthCheckData.Count > 0) + { + _logger.LogInformation("HealCheck data obtained."); + + var healthCheckDocument = JsonBuilder.BuildJsonObject(healthCheckData); + + /// Step 4. Post the HealthCheck data to the Admin API + HealthCheckCommandModel healthCheckCommandModel = new( + instance.TenantId, + instance.Id, + healthCheckDocument.ToString() + ); + _logger.LogInformation("Posting HealthCheck data to Admin Api."); + + await _addHealthCheckCommand.Execute(healthCheckCommandModel); + } + else + { + _logger.LogInformation( + "No HealthCheck data has been collected for instance with name: {InstanceName}", + instance.InstanceName + ); + } } } } } - } - _logger.LogInformation("Process completed."); + _logger.LogInformation("Process completed."); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while running the HealthCheck Service."); } static string GetTenantName(TenantModel tenant) @@ -140,16 +145,6 @@ static AdminConsoleInstance ConvertToAdminConsoleInstance(Instance instance) }; } } - - public async Task StartAsync(CancellationToken cancellationToken) - { - await Run(); - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - await Task.CompletedTask; - } } public class HealthCheckCommandModel(int tenantId, int instanceId, string document) : IAddHealthCheckModel diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs index f233eabcb..4162f6057 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs @@ -4,14 +4,12 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using EdFi.AdminConsole.HealthCheckService.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using Polly.Contrib.WaitAndRetry; -using Serilog.Core; using Constants = EdFi.AdminConsole.HealthCheckService.Helpers.Constants; namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; @@ -23,10 +21,10 @@ public interface IAppHttpClient Task SendAsync(string uriString, HttpMethod method, FormUrlEncodedContent content, AuthenticationHeaderValue? authenticationHeaderValue); } -public class AppHttpClient(HttpClient httpClient, ILogger logger, IOptions options) : IAppHttpClient +public class AppHttpClient(HttpClient httpClient, ILogger logger, IOptions options) : IAppHttpClient { private readonly HttpClient _httpClient = httpClient; - protected readonly ILogger _logger = logger; + protected readonly ILogger _logger = logger; protected readonly AppSettings _options = options.Value; public async Task SendAsync(string uriString, HttpMethod method, StringContent? content, AuthenticationHeaderValue? authenticationHeaderValue) diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Models/IInstanceRequestModel.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Models/IInstanceRequestModel.cs index c79aa39b2..de7b68254 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Models/IInstanceRequestModel.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Models/IInstanceRequestModel.cs @@ -17,7 +17,7 @@ public interface IInstanceRequestModel ICollection? OdsInstanceDerivatives { get; } byte[]? Credentials { get; } - public string? Status { get; set; } + string? Status { get; set; } } public class OdsInstanceContextModel diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs index 4b122bfa8..177f96bf6 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs @@ -21,11 +21,11 @@ public interface IAdminConsoleTenantsService Task GetTenantByTenantIdAsync(int tenantId); } -public class TenantService(IOptionsSnapshot options, - IMemoryCache memoryCache) : IAdminConsoleTenantsService +public class TenantService(IOptionsMonitor options, IMemoryCache memoryCache) + : IAdminConsoleTenantsService { private const string ADMIN_DB_KEY = "EdFi_Admin"; - protected AppSettingsFile _appSettings = options.Value; + protected AppSettingsFile _appSettings = options.CurrentValue; private readonly IMemoryCache _memoryCache = memoryCache; private static readonly ILog _log = LogManager.GetLogger(typeof(TenantService)); diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs index 62b3c2940..14d0f4b29 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs @@ -8,14 +8,14 @@ namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; -public class HealthCheckJob(IApplication application, ILogger logger) : IJob +public class HealthCheckJob(IHealthCheckService healthCheckService, ILogger logger) : IJob { - private readonly IApplication _application = application; + private readonly IHealthCheckService _healthCheckService = healthCheckService; private readonly ILogger _logger = logger; public async Task Execute(IJobExecutionContext context) { _logger.LogInformation("Running scheduled health check..."); - await _application.Run(); + await _healthCheckService.Run(); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs index b5be2f563..21edb933e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -3,45 +3,34 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService; using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; using EdFi.AdminConsole.HealthCheckService.Infrastructure; -using EdFi.AdminConsole.HealthCheckService; -using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; -using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; public static class HealthCheckServiceExtension { - public static IServiceCollection ConfigureHealthCheckServices( - this IServiceCollection services, - IConfiguration configuration - ) + public static void ConfigureHealthCheckServices( + this WebApplicationBuilder builder, + IConfiguration configuration + ) { - services.AddOptions(); - services.Configure(configuration.GetSection("AppSettings")); - services.Configure(configuration.GetSection("OdsApiSettings")); - -#pragma warning disable CS8603 // Possible null reference return. - services.AddSingleton(sp => sp.GetService>()); -#pragma warning restore CS8603 // Possible null reference return. + builder.Services.AddOptions(); + builder.Services.Configure(configuration.GetSection("AppSettings")); + builder.Services.Configure(configuration.GetSection("OdsApiSettings")); - services.AddSingleton(); - services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - services.AddTransient(); - - services + builder.Services .AddHttpClient( "AppHttpClient", x => @@ -60,8 +49,6 @@ IConfiguration configuration } return handler; }); - - return services; } private static HttpClientHandler IgnoresCertificateErrorsHandler() diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index 965eb048e..6876b4a7d 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -218,27 +218,25 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); - webApplicationBuilder.Services.ConfigureHealthCheckServices( - webApplicationBuilder.Configuration - ); + var adminConsoleIsEnabled = webApplicationBuilder.Configuration.GetValue("AppSettings:EnableAdminConsoleAPI"); - // Quartz.NET back end service - webApplicationBuilder.Services.AddQuartz(q => + if (adminConsoleIsEnabled) { - var jobKey = new JobKey("HealthCheckJob"); - q.AddJob(opts => opts.WithIdentity(jobKey)); - - // Create a trigger that fires every 10 minutes - q.AddTrigger(opts => opts - .ForJob(jobKey) - .WithIdentity("HealthCheckJob-trigger") - .WithSimpleSchedule(x => x - .WithInterval(TimeSpan.FromMinutes(2)) - .RepeatForever()) - ); - }); - - webApplicationBuilder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + // Quartz.NET back end service + webApplicationBuilder.Services.AddQuartz(q => + { + var jobKey = new JobKey("HealthCheckJob"); + q.AddJob(opts => opts.WithIdentity(jobKey)); + + // Create a trigger that fires every 10 minutes + q.AddTrigger(opts => + opts.ForJob(jobKey) + .WithIdentity("HealthCheckJob-trigger") + .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMinutes(2)).RepeatForever()) + ); + }); + webApplicationBuilder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + } } private static void EnableMultiTenancySupport(this WebApplicationBuilder webApplicationBuilder) @@ -451,7 +449,12 @@ public static void ConfigureRateLimiting(WebApplicationBuilder builder) var parts = rule.Endpoint.Split(':'); // Only support fixed window for now, parse period (e.g., "1m") var window = rule.Period.EndsWith('m') ? TimeSpan.FromMinutes(int.Parse(rule.Period.TrimEnd('m'))) : TimeSpan.FromMinutes(1); - if (path != null && parts.Length == 2 && method.Equals(parts[0], StringComparison.OrdinalIgnoreCase) && path.Equals(parts[1], StringComparison.OrdinalIgnoreCase)) + if ( + path != null + && parts.Length == 2 + && method.Equals(parts[0], StringComparison.OrdinalIgnoreCase) + && path.Equals(parts[1], StringComparison.OrdinalIgnoreCase) + ) { return RateLimitPartition.GetFixedWindowLimiter(rule.Endpoint, _ => new FixedWindowRateLimiterOptions { diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index f33fd09dd..1bc4919b3 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -9,6 +9,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Features; using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; using log4net; var builder = WebApplication.CreateBuilder(args); @@ -25,7 +26,10 @@ builder.AddServices(); if (adminConsoleIsEnabled) +{ builder.RegisterAdminConsoleDependencies(); + builder.ConfigureHealthCheckServices(builder.Configuration); +} var app = builder.Build(); diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs b/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs index 47c4f7e7a..e2327cecf 100644 --- a/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs +++ b/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs @@ -46,7 +46,7 @@ public static IOptions GetAppSettings() return Options.Create(appSettings); } - public static IOptionsSnapshot GetOptionsSnapshot() + public static IOptionsMonitor GetOptionsSnapshot() { var appSettingsFile = new AppSettingsFile { @@ -100,8 +100,8 @@ public static IOptionsSnapshot GetOptionsSnapshot() } }; - var optionsSnapshot = A.Fake>(); - A.CallTo(() => optionsSnapshot.Value).Returns(appSettingsFile); + var optionsSnapshot = A.Fake>(); + A.CallTo(() => optionsSnapshot.CurrentValue).Returns(appSettingsFile); return optionsSnapshot; } From a3ff0c8a75be6c86e5f24606b8e47aee2d47f243 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Thu, 31 Jul 2025 18:32:10 -0500 Subject: [PATCH 06/14] Add health check endpoint --- .../AppSettings.cs | 8 +-- .../HealthCheckService.cs | 4 +- .../HealthCheck/HealthCheckTrigger.cs | 53 +++++++++++++++++++ .../BackgroundJobs/HealthCheckJob.cs | 3 +- .../WebApplicationBuilderExtensions.cs | 5 +- .../EdFi.Ods.AdminApi/appsettings.json | 3 +- 6 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs diff --git a/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs b/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs index bbbd2a587..49af5fab3 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs @@ -5,13 +5,7 @@ namespace EdFi.AdminConsole.HealthCheckService; -public interface IAppSettings +public sealed class AppSettings { - bool IgnoresCertificateErrors { get; set; } -} - -public sealed class AppSettings : IAppSettings -{ - public bool IgnoresCertificateErrors { get; set; } = false; public int MaxRetryAttempts { get; set; } } diff --git a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs index 3c2841a27..28da0fefd 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs @@ -21,7 +21,7 @@ namespace EdFi.AdminConsole.HealthCheckService; public interface IHealthCheckService { - Task Run(); + Task RunAsync(CancellationToken cancellationToken); } public class HealthCheckService(ILogger logger, @@ -37,7 +37,7 @@ public class HealthCheckService(ILogger logger, private readonly IOdsApiCaller _odsApiCaller = odsApiCaller; private readonly IAddHealthCheckCommand _addHealthCheckCommand = addHealthCheckCommand; - public async Task Run() + public async Task RunAsync(CancellationToken cancellationToken) { try { diff --git a/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs b/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs new file mode 100644 index 000000000..183afd8c1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; +using Quartz; + +namespace EdFi.Ods.AdminApi.Features.HealthCheck; + +public class HealthCheckTrigger : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + var config = endpoints.ServiceProvider.GetRequiredService(); + bool enabled = config.GetValue("AppSettings:EnableAdminConsoleAPI"); + if (enabled) + { + AdminApiEndpointBuilder.MapPost(endpoints, "/healthcheck/trigger", TriggerHealthCheck) + .WithRouteOptions(b => b.WithResponseCode(202)) + .AllowAnonymous() + .BuildForVersions(AdminApiVersions.AdminConsole); + } + } + + internal static async Task TriggerHealthCheck(ISchedulerFactory schedulerFactory) + { + var scheduler = await schedulerFactory.GetScheduler(); + var jobKey = new JobKey("HealthCheckJob"); + + if (!await scheduler.CheckExists(jobKey)) + { + var jobDetail = JobBuilder.Create() + .WithIdentity(jobKey) + .Build(); + await scheduler.AddJob(jobDetail, replace: true); + } + + // Fire-and-forget: schedule a one-time immediate trigger + var trigger = TriggerBuilder.Create() + .ForJob(jobKey) + .WithIdentity($"ImmediateTrigger-{Guid.NewGuid()}") + .StartNow() + .Build(); + + await scheduler.ScheduleJob(trigger); + + // Return accepted immediately without waiting for execution + return Results.Accepted("/healthcheck/trigger", new { Title = "Health check process accepted and triggered. The latest results will be available shortly, and processing details will be logged for your reference.", Status = 202 }); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs index 14d0f4b29..5ff556164 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs @@ -8,6 +8,7 @@ namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; +[DisallowConcurrentExecution] public class HealthCheckJob(IHealthCheckService healthCheckService, ILogger logger) : IJob { private readonly IHealthCheckService _healthCheckService = healthCheckService; @@ -16,6 +17,6 @@ public class HealthCheckJob(IHealthCheckService healthCheckService, ILogger(); var adminConsoleIsEnabled = webApplicationBuilder.Configuration.GetValue("AppSettings:EnableAdminConsoleAPI"); + var healthCheckFrequency = webApplicationBuilder.Configuration.GetValue("AppSettings:HealthCheckFrequencyInMinutes"); if (adminConsoleIsEnabled) { @@ -232,10 +233,10 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) q.AddTrigger(opts => opts.ForJob(jobKey) .WithIdentity("HealthCheckJob-trigger") - .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMinutes(2)).RepeatForever()) + .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromMinutes(healthCheckFrequency)).RepeatForever()) ); }); - webApplicationBuilder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); + webApplicationBuilder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = false); } } diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 07b4a09b0..2efda569e 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -8,8 +8,9 @@ "MultiTenancy": false, "PreventDuplicateApplications": false, "EnableAdminConsoleAPI": false, - "IgnoresCertificateErrors": false, "EnableApplicationResetEndpoint": true + "HealthCheckFrequencyInMinutes": 10, + "IgnoresCertificateErrors": false }, "AdminApiSettings": { "AdminConsoleTenantsURL": "https://localhost/adminapi/adminconsole/tenants", From 34b70c0ce86b19d089e580fdc142b65e43f72a27 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 12:10:38 -0500 Subject: [PATCH 07/14] Add health check unit tests --- Application/Ed-Fi-ODS-AdminApi.sln | 10 + ...onsole.HealthCheckService.UnitTests.csproj | 26 + .../Features/OdsApi/OdsApiCallerTests.cs | 59 ++ .../Features/OdsApi/OdsApiClientTests.cs | 84 +++ .../HealthCheckServiceTests.cs | 604 ++++++++++++++++++ .../Helpers/InstanceValidatorTests.cs | 90 +++ .../Helpers/JsonBuilderTests.cs | 73 +++ .../Helpers/TestLoggerProvider.cs | 59 ++ .../Testing.cs | 137 ++++ .../HealthCheckService.cs | 2 +- .../HealthCheckServiceExtension.cs | 2 +- .../WebApplicationBuilderExtensions.cs | 6 +- .../EdFi.Ods.AdminApi/appsettings.json | 39 +- 13 files changed, 1163 insertions(+), 28 deletions(-) create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index 25db6951a..f316fbd1d 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.AdminCons EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.AdminConsole.HealthCheckService", "EdFi.AdminConsole.HealthCheckService\EdFi.AdminConsole.HealthCheckService.csproj", "{89457FDC-0611-4703-DA46-DEDD38C5FE22}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.AdminConsole.HealthCheckService.UnitTests", "EdFi.AdminConsole.HealthCheckService.UnitTests\EdFi.AdminConsole.HealthCheckService.UnitTests.csproj", "{16AC7821-B294-1CEF-A93E-3164AC9CBCE9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -112,6 +114,14 @@ Global {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|Any CPU.Build.0 = Release|Any CPU {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.ActiveCfg = Release|Any CPU {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.Build.0 = Release|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|x64.ActiveCfg = Debug|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|x64.Build.0 = Debug|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|Any CPU.Build.0 = Release|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|x64.ActiveCfg = Release|Any CPU + {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj new file mode 100644 index 000000000..02de1553c --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs new file mode 100644 index 000000000..a721fdbc9 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Features.OdsApi; + +public class Given_an_ods_api +{ + [TestFixture] + public class When_HealthCheckData_is_returned_from_api : Given_an_ods_api + { + private IOdsApiClient _odsApiClient; + private OdsApiCaller _odsApiCaller; + + [SetUp] + public void SetUp() + { + _odsApiClient = A.Fake(); + + var adminApiInstance = Testing.AdminApiInstances.First(); + + var httpResponse1 = new HttpResponseMessage(HttpStatusCode.OK); + httpResponse1.Headers.Add(Constants.TotalCountHeader, "3"); + + var httpResponse2 = new HttpResponseMessage(HttpStatusCode.OK); + httpResponse2.Headers.Add(Constants.TotalCountHeader, "8"); + + var httpResponse3 = new HttpResponseMessage(HttpStatusCode.OK); + httpResponse3.Headers.Add(Constants.TotalCountHeader, "5"); + + A.CallTo(() => _odsApiClient.OdsApiGet(A.Ignored, A.Ignored, A.Ignored, "http://www.myserver.com/data/v3/ed-fi/firstEndPoint?offset=0&limit=0&totalCount=true")) + .Returns(new ApiResponse(HttpStatusCode.OK, string.Empty, httpResponse1.Headers)); + + A.CallTo(() => _odsApiClient.OdsApiGet(A.Ignored, A.Ignored, A.Ignored, "http://www.myserver.com/data/v3/ed-fi/secondEndpoint?offset=0&limit=0&totalCount=true")) + .Returns(new ApiResponse(HttpStatusCode.OK, string.Empty, httpResponse2.Headers)); + + A.CallTo(() => _odsApiClient.OdsApiGet(A.Ignored, A.Ignored, A.Ignored, "http://www.myserver.com/data/v3/ed-fi/thirdEndPoint?offset=0&limit=0&totalCount=true")) + .Returns(new ApiResponse(HttpStatusCode.OK, string.Empty, httpResponse3.Headers)); + + _odsApiCaller = new OdsApiCaller(_odsApiClient, new AppSettingsOdsApiEndpoints(Testing.GetOdsApiSettings())); + } + + [Test] + public async Task should_return_stronglytyped_healthCheck_data() + { + var healthCheckData = await _odsApiCaller.GetHealthCheckDataAsync(Testing.AdminApiInstances.First()); + healthCheckData.ShouldBeEquivalentTo(Testing.HealthCheckData); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs new file mode 100644 index 000000000..cb816507f --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Features.OdsApi; + +public class Given_an_ods_environment_with_single_tenant +{ + private ILogger _logger; + + [SetUp] + public void SetUp() + { + _logger = A.Fake>(); + } + + public class When_HealthCheck_data_is_requested : Given_an_ods_environment_with_single_tenant + { + [Test] + public async Task should_return_successfully() + { + var httpClient = A.Fake(); + var adminApiInstance = Testing.AdminApiInstances.First(); + var encodedKeySecret = Encoding.ASCII.GetBytes($"{adminApiInstance.ClientId}:{adminApiInstance.ClientSecret}"); + var headers = new HttpResponseMessage().Headers; + headers.Add(Constants.TotalCountHeader, "5"); + + A.CallTo(() => httpClient.SendAsync( + adminApiInstance.OauthUrl, HttpMethod.Post, A.Ignored, new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encodedKeySecret)))) + .Returns(new ApiResponse(HttpStatusCode.OK, "{ \"access_token\": \"123\"}")); + + A.CallTo(() => httpClient.SendAsync(adminApiInstance.ResourceUrl, HttpMethod.Get, null as StringContent, new AuthenticationHeaderValue("bearer", "123"))) + .Returns(new ApiResponse(HttpStatusCode.OK, string.Empty, headers)); + + var odsApiClient = new OdsApiClient(httpClient, _logger, Testing.GetAppSettings()); + + var response = await odsApiClient.OdsApiGet( + adminApiInstance.OauthUrl, adminApiInstance.ClientId, adminApiInstance.ClientSecret, adminApiInstance.ResourceUrl); + + response.Headers.ShouldNotBeNull(); + response.Headers.Any(o => o.Key == Constants.TotalCountHeader).ShouldBe(true); + response.Headers.GetValues(Constants.TotalCountHeader).First().ShouldBe("5"); + } + } + + public class When_HealthCheck_data_is_requested_without_token : Given_an_ods_environment_with_single_tenant + { + [Test] + public async Task InternalServerError_is_returned() + { + var httpClient = A.Fake(); + var adminApiInstance = Testing.AdminApiInstances.First(); + var encodedKeySecret = Encoding.ASCII.GetBytes($"{adminApiInstance.ClientId}:{adminApiInstance.ClientSecret}"); + + var headers = new HttpResponseMessage().Headers; + headers.Add(Constants.TotalCountHeader, "5"); + + A.CallTo(() => httpClient.SendAsync( + adminApiInstance.OauthUrl, HttpMethod.Post, A.Ignored, new AuthenticationHeaderValue("Basic", Convert.ToBase64String(encodedKeySecret)))) + .Returns(new ApiResponse(HttpStatusCode.InternalServerError, string.Empty)); + + A.CallTo(() => httpClient.SendAsync(adminApiInstance.ResourceUrl, HttpMethod.Get, null as StringContent, new AuthenticationHeaderValue("bearer", "123"))) + .Returns(new ApiResponse(HttpStatusCode.OK, string.Empty, headers)); + + var odsApiClient = new OdsApiClient(httpClient, _logger, Testing.GetAppSettings()); + + var response = await odsApiClient.OdsApiGet(adminApiInstance.OauthUrl, adminApiInstance.ClientId, adminApiInstance.ClientSecret, adminApiInstance.ResourceUrl); + + response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs new file mode 100644 index 000000000..2e9acf1d5 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs @@ -0,0 +1,604 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Dynamic; +using System.Text; +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; +using EdFi.Ods.AdminApi.AdminConsole.Features.WorkerInstances; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using System.Text.Json; +using NUnit.Framework; +using Shouldly; +using EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests; + +public class Given_a_health_check_service +{ + public static List CreateTestTenants() + { + var tenant = new TenantModel { TenantId = 1 }; + var expandoObject = new ExpandoObject(); + var dict = (IDictionary)expandoObject; + dict["name"] = "TestTenant"; + tenant.Document = expandoObject; + return [tenant]; + } + + [TestFixture] + public class When_running_health_check_with_valid_tenants_and_instances : Given_a_health_check_service + { + private ILogger _logger; + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + + [SetUp] + public void SetUp() + { + _logger = A.Fake>(); + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + // Setup test data + var tenants = CreateTestTenants(); + var instances = CreateTestInstances(); + var healthCheckData = CreateTestHealthCheckData(); + + var savedHealthCheck = new HealthCheck + { + DocId = 1, + InstanceId = 1, + EdOrgId = 255, + TenantId = 1, + Document = JsonSerializer.Serialize(healthCheckData).ToString() + }; + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Returns(tenants); + + A.CallTo(() => _getInstancesQuery.Execute("TestTenant", "Completed")) + .Returns(instances); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.That.Matches(i => i.Id == 1))) + .Returns(Task.FromResult(healthCheckData)); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.Ignored)) + .Returns(savedHealthCheck); + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_call_get_tenants_service() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Should_call_get_instances_query_for_each_tenant() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _getInstancesQuery.Execute("TestTenant", "Completed")) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Should_call_ods_api_caller_for_valid_instances() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.That.Matches(i => i.Id == 1))) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task Should_call_add_health_check_command_with_correct_data() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.That.Matches(cmd => + cmd.TenantId == 1 && + cmd.InstanceId == 1 && + !string.IsNullOrEmpty(cmd.Document)))) + .MustHaveHappenedOnceExactly(); + } + + private static List CreateTestInstances() + { + var credentials = new InstanceWorkerModelDto + { + ClientId = "test-client-id", + Secret = "test-secret" + }; + + return + [ + new Instance + { + Id = 1, + TenantId = 1, + InstanceName = "TestInstance", + ResourceUrl = "http://test-resource-url.com", + OAuthUrl = "http://test-oauth-url.com", + Credentials = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(credentials)) + } + ]; + } + + private static List CreateTestHealthCheckData() + { + return + [ + new OdsApiEndpointNameCount { OdsApiEndpointName="students", OdsApiEndpointCount=100 }, + new OdsApiEndpointNameCount { OdsApiEndpointName="schools", OdsApiEndpointCount=10 }, + new OdsApiEndpointNameCount { OdsApiEndpointName="staff", OdsApiEndpointCount=50 } + ]; + } + } + + [TestFixture] + public class When_running_health_check_with_no_tenants : Given_a_health_check_service + { + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + public TestLoggerProvider _testLoggerProvider; + public ILogger _logger; + + [TearDown] + public void TearDown() + { + _testLoggerProvider?.Dispose(); + } + + [SetUp] + public void SetUp() + { + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + _testLoggerProvider = new TestLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(b => b.AddProvider(_testLoggerProvider)); + _logger = loggerFactory.CreateLogger(); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Returns([]); + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_log_no_tenants_message() + { + await _healthCheckService.RunAsync(_cancellationToken); + + var information = _testLoggerProvider.Entries.FirstOrDefault(e => + e.LogLevel == LogLevel.Information && + e.Message != null && e.Message.Contains("No tenants returned from Admin Api")); + + information.ShouldNotBeNull(); + } + + [Test] + public async Task Should_not_call_get_instances_query() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _getInstancesQuery.Execute(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Test] + public async Task Should_not_call_ods_api_caller() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Test] + public async Task Should_not_call_add_health_check_command() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.Ignored)) + .MustNotHaveHappened(); + } + } + + [TestFixture] + public class When_running_health_check_with_no_instances : Given_a_health_check_service + { + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + public TestLoggerProvider _testLoggerProvider; + public ILogger _logger; + + [TearDown] + public void TearDown() + { + _testLoggerProvider?.Dispose(); + } + + [SetUp] + public void SetUp() + { + _logger = A.Fake>(); + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + _testLoggerProvider = new TestLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(b => b.AddProvider(_testLoggerProvider)); + _logger = loggerFactory.CreateLogger(); + + var tenants = CreateTestTenants(); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Returns(tenants); + + A.CallTo(() => _getInstancesQuery.Execute("TestTenant", "Completed")) + .Returns([]); + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_log_no_instances_message() + { + await _healthCheckService.RunAsync(_cancellationToken); + + var information = _testLoggerProvider.Entries.FirstOrDefault(e => + e.LogLevel == LogLevel.Information && + e.Message != null && e.Message.Contains("No instances found on Admin Api")); + + information.ShouldNotBeNull(); + } + + [Test] + public async Task Should_not_call_ods_api_caller() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Test] + public async Task Should_not_call_add_health_check_command() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.Ignored)) + .MustNotHaveHappened(); + } + } + + [TestFixture] + public class When_running_health_check_with_invalid_instance : Given_a_health_check_service + { + private ILogger _logger; + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + + [SetUp] + public void SetUp() + { + _logger = A.Fake>(); + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + var tenants = CreateTestTenants(); + var invalidInstances = CreateInvalidTestInstances(); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Returns(tenants); + + A.CallTo(() => _getInstancesQuery.Execute("TestTenant", "Completed")) + .Returns(invalidInstances); + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_not_call_ods_api_caller_for_invalid_instance() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.Ignored)) + .MustNotHaveHappened(); + } + + [Test] + public async Task Should_not_call_add_health_check_command_for_invalid_instance() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.Ignored)) + .MustNotHaveHappened(); + } + + private static List CreateInvalidTestInstances() + { + return + [ + new Instance + { + Id = 1, + TenantId = 1, + InstanceName = "InvalidInstance", + ResourceUrl = "", // Invalid - empty resource URL + OAuthUrl = "", // Invalid - empty OAuth URL + Credentials = null // Invalid - no credentials + } + ]; + } + } + + [TestFixture] + public class When_running_health_check_with_no_health_check_data : Given_a_health_check_service + { + private ILogger _logger; + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + private TestLoggerProvider _testLoggerProvider; + + [TearDown] + public void TearDown() + { + _testLoggerProvider?.Dispose(); + } + + [SetUp] + public void SetUp() + { + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + _testLoggerProvider = new TestLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(b => b.AddProvider(_testLoggerProvider)); + _logger = loggerFactory.CreateLogger(); + + var tenants = CreateTestTenants(); + var instances = CreateTestInstances(); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Returns(tenants); + + A.CallTo(() => _getInstancesQuery.Execute("TestTenant", "Completed")) + .Returns(instances); + + A.CallTo(() => _odsApiCaller.GetHealthCheckDataAsync(A.Ignored)) + .Returns([]); // Empty health check data + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_log_no_health_check_data_message() + { + await _healthCheckService.RunAsync(_cancellationToken); + + var information = _testLoggerProvider.Entries.FirstOrDefault(e => + e.LogLevel == LogLevel.Information && + e.Message != null && e.Message.Contains("No HealthCheck data has been collected")); + + information.ShouldNotBeNull(); + } + + [Test] + public async Task Should_not_call_add_health_check_command() + { + await _healthCheckService.RunAsync(_cancellationToken); + + A.CallTo(() => _addHealthCheckCommand.Execute(A.Ignored)) + .MustNotHaveHappened(); + } + + private static List CreateTestInstances() + { + var credentials = new InstanceWorkerModelDto + { + ClientId = "test-client-id", + Secret = "test-secret" + }; + + return + [ + new Instance + { + Id = 1, + TenantId = 1, + InstanceName = "TestInstance", + ResourceUrl = "http://test-resource-url.com", + OAuthUrl = "http://test-oauth-url.com", + Credentials = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(credentials)) + } + ]; + } + } + + [TestFixture] + public class When_running_health_check_with_exception : Given_a_health_check_service + { + private ILogger _logger; + private IAdminConsoleTenantsService _adminConsoleTenantsService; + private IGetInstancesQuery _getInstancesQuery; + private IAddHealthCheckCommand _addHealthCheckCommand; + private IOdsApiCaller _odsApiCaller; + private HealthCheckService _healthCheckService; + private CancellationToken _cancellationToken; + private TestLoggerProvider _testLoggerProvider; + + [TearDown] + public void TearDown() + { + _testLoggerProvider?.Dispose(); + } + + [SetUp] + public void SetUp() + { + _adminConsoleTenantsService = A.Fake(); + _getInstancesQuery = A.Fake(); + _addHealthCheckCommand = A.Fake(); + _odsApiCaller = A.Fake(); + _cancellationToken = CancellationToken.None; + + _testLoggerProvider = new TestLoggerProvider(); + using var loggerFactory = LoggerFactory.Create(b => b.AddProvider(_testLoggerProvider)); + _logger = loggerFactory.CreateLogger(); + + A.CallTo(() => _adminConsoleTenantsService.GetTenantsAsync(true)) + .Throws(new Exception("Test exception")); + + _healthCheckService = new HealthCheckService( + _logger, + _adminConsoleTenantsService, + _getInstancesQuery, + _addHealthCheckCommand, + _odsApiCaller); + } + + [Test] + public async Task Should_log_error_when_exception_occurs() + { + await _healthCheckService.RunAsync(_cancellationToken); + + var error = _testLoggerProvider.Entries.FirstOrDefault(e => + e.LogLevel == LogLevel.Error && + e.Message != null && e.Message.Contains("An error occurred while running the HealthCheck Service.") + && e.Exception != null && e.Exception.Message == "Test exception"); + + error.ShouldNotBeNull(); + } + + [Test] + public async Task Should_not_throw_exception() + { + Should.NotThrow(async () => await _healthCheckService.RunAsync(_cancellationToken)); + } + } +} + +public class Given_a_health_check_command_model +{ + [TestFixture] + public class When_creating_health_check_command_model : Given_a_health_check_command_model + { + [Test] + public void Should_set_properties_correctly() + { + const int TenantId = 1; + const int InstanceId = 2; + const string Document = "test document"; + + var model = new HealthCheckCommandModel(TenantId, InstanceId, Document); + + model.TenantId.ShouldBe(TenantId); + model.InstanceId.ShouldBe(InstanceId); + model.Document.ShouldBe(Document); + model.DocId.ShouldBe(0); // Default value + model.EdOrgId.ShouldBe(0); // Default value + } + + [Test] + public void Should_implement_IAddHealthCheckModel() + { + var model = new HealthCheckCommandModel(1, 2, "document"); + + model.ShouldBeAssignableTo(); + } + + [Test] + public void Should_allow_property_modification() + { + var model = new HealthCheckCommandModel(1, 2, "document") + { + TenantId = 10, + InstanceId = 20, + Document = "new document", + DocId = 30, + EdOrgId = 40 + }; + + model.TenantId.ShouldBe(10); + model.InstanceId.ShouldBe(20); + model.Document.ShouldBe("new document"); + model.DocId.ShouldBe(30); + model.EdOrgId.ShouldBe(40); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs new file mode 100644 index 000000000..a6f0fd4e6 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; + +public class Given_an_instance_returned_from_AdminApi +{ + private AdminConsoleInstance _instance = new AdminConsoleInstance(); + private ILogger _logger; + + [SetUp] + public void SetUp() + { + _logger = A.Fake>(); + + _instance.OauthUrl = "Some url"; + _instance.ResourceUrl = "Some url"; + _instance.ClientId = "Some url"; + _instance.ClientSecret = "Some url"; + _instance.Id = 1; + _instance.TenantId = 1; + _instance.InstanceName = "Some url"; + } + + [TestFixture] + public class When_it_has_all_required_fields : Given_an_instance_returned_from_AdminApi + { + [Test] + public void should_be_valid() + { + InstanceValidator.IsInstanceValid(_logger, _instance).ShouldBeTrue(); + } + } + + [TestFixture] + public class When_it_does_not_have_AuthenticationUrl : Given_an_instance_returned_from_AdminApi + { + [Test] + public void should_be_invalid() + { + _instance.OauthUrl = string.Empty; + InstanceValidator.IsInstanceValid(_logger, _instance).ShouldBeFalse(); + } + } + + [TestFixture] + public class When_it_does_not_have_ResourceUrl : Given_an_instance_returned_from_AdminApi + { + + [Test] + public void should_be_invalid() + { + _instance.ResourceUrl = string.Empty; + InstanceValidator.IsInstanceValid(_logger, _instance).ShouldBeFalse(); + } + } + + + [TestFixture] + public class When_it_does_not_have_ClientId : Given_an_instance_returned_from_AdminApi + { + [Test] + public void should_be_invalid() + { + _instance.ClientId = string.Empty; + InstanceValidator.IsInstanceValid(_logger, _instance).ShouldBeFalse(); + } + } + + [TestFixture] + public class When_it_does_not_have_ClientSecret : Given_an_instance_returned_from_AdminApi + { + + [Test] + public void should_be_invalid() + { + _instance.ClientSecret = string.Empty; + InstanceValidator.IsInstanceValid(_logger, _instance).ShouldBeFalse(); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs new file mode 100644 index 000000000..b37d6fc47 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.AdminConsole.HealthCheckService.Helpers; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; + +public class Given_a_set_of_healthCheck_data +{ + private List _endpoointCounts = new List(); + + [SetUp] + public void SetUp() + { + _endpoointCounts = new List() + { + new OdsApiEndpointNameCount() + { + OdsApiEndpointName = "Some endpoint", + OdsApiEndpointCount = 2, + AnyErrros = false + }, + new OdsApiEndpointNameCount() + { + OdsApiEndpointName = "Other endpoint", + OdsApiEndpointCount = 3, + AnyErrros = false + } + }; + } + + [TestFixture] + public class When_a_json_is_built : Given_a_set_of_healthCheck_data + { + [Test] + public void should_be_valid() + { + var expectedHealthCheckJsonObjectPayload = "{\"healthy\": true,\"Some endpoint\": 2,\"Other endpoint\": 3}"; + + var healthCheckJsonObjectPayload = JsonBuilder.BuildJsonObject(_endpoointCounts); + + JObject.Parse(healthCheckJsonObjectPayload.ToString()).ShouldBeEquivalentTo(JObject.Parse(expectedHealthCheckJsonObjectPayload.ToString())); + } + } + + [TestFixture] + public class When_a_json_is_built_with_errors : Given_a_set_of_healthCheck_data + { + [Test] + public void should_be_invalid() + { + var expectedHealthCheckJsonObjectPayload = "{\"healthy\": false,\"Some endpoint\": 2,\"Other endpoint\": 3,\"One more endpoint\": 0}"; + + var endpoointCountsWithErrors = _endpoointCounts; + endpoointCountsWithErrors.Add(new OdsApiEndpointNameCount + { + OdsApiEndpointName = "One more endpoint", + OdsApiEndpointCount = 0, + AnyErrros = true + }); + + var healthCheckJsonObjectPayload = JsonBuilder.BuildJsonObject(endpoointCountsWithErrors); + + JObject.Parse(healthCheckJsonObjectPayload.ToString()).ShouldBeEquivalentTo(JObject.Parse(expectedHealthCheckJsonObjectPayload.ToString())); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs new file mode 100644 index 000000000..9fc06fd62 --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.Extensions.Logging; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; +public class TestLoggerProvider : ILoggerProvider +{ + private readonly List _entries = new(); + public IReadOnlyList Entries => _entries; + + public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, _entries); + public void Dispose() { } + + private class TestLogger(string category, List entries) : ILogger + { + private readonly List _entries = entries; + private readonly string _category = category; + + public IDisposable BeginScope(TState state) => NullScope.Instance; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func formatter) + { + _entries.Add(new LogEntry + { + LogLevel = logLevel, + EventId = eventId, + Message = formatter(state, exception), + Exception = exception, + State = state, + Category = _category + }); + } + } + + private class NullScope : IDisposable + { + public static NullScope Instance { get; } = new(); + public void Dispose() { } + } + + public record LogEntry + { + public string? Category { get; init; } + public LogLevel LogLevel { get; init; } + public EventId EventId { get; init; } + public string? Message { get; init; } + public Exception? Exception { get; init; } + public object? State { get; init; } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs new file mode 100644 index 000000000..480ffcecd --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using Microsoft.Extensions.Options; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests; + +public class Testing +{ + public static IOptions GetAppSettings() + { + IOptions options = Options.Create(new AppSettings()); + return options; + } + + public static IOptions GetOdsApiSettings() + { + OdsApiSettings odsApiSettings = new() + { + Endpoints = Endpoints + }; + IOptions options = Options.Create(odsApiSettings); + return options; + } + + public static List Endpoints { get { return ["firstEndPoint", "secondEndpoint", "thirdEndPoint"]; } } + + public static List HealthCheckData + { + get + { + return + [ + new OdsApiEndpointNameCount() + { + OdsApiEndpointName = "firstEndPoint", + OdsApiEndpointCount = 3, + AnyErrros = false + }, + new OdsApiEndpointNameCount() + { + OdsApiEndpointName = "secondEndpoint", + OdsApiEndpointCount = 8, + AnyErrros = false + }, + new OdsApiEndpointNameCount() + { + OdsApiEndpointName = "thirdEndPoint", + OdsApiEndpointCount = 5, + AnyErrros = false + } + ]; + } + } + + public const string Tenants = + @"[{ + ""TenantId"": 1, + ""Document"": + { + ""EdfiApiDiscoveryUrl"": ""https://api.ed-fi.org/v7.1/api6/"", + ""Name"" : ""tenant1"" + } + },{ + ""TenantId"": 2, + ""Document"": + { + ""EdfiApiDiscoveryUrl"": ""https://api.ed-fi.org/v7.2/api6/"", + ""Name"" : ""tenant2"" + } + }]"; + + public static List AdminApiInstances + { + get + { + return + [ + new() + { + Id = 1, + OdsInstanceId = 1, + TenantId = 1, + TenantName = "tenant1", + InstanceName = "instance 1", + ClientId = "one client", + ClientSecret = "one secret", + OauthUrl = "http://www.myserver.com/connect/token", + ResourceUrl = "http://www.myserver.com/data/v3/", + Status = "Completed", + }, + new() + { + Id = 2, + OdsInstanceId = 2, + TenantId = 1, + TenantName = "tenant1", + InstanceName = "instance 2", + ClientId = "another client", + ClientSecret = "another secret", + OauthUrl = "http://www.myserver.com/connect/token", + ResourceUrl = "http://www.myserver.com/data/v3/", + Status = "Completed", + } + ]; + } + } + + public const string Instances = + @"[{ + ""id"": 1, + ""odsInstanceId"": 1, + ""tenantId"": 1, + ""tenantName"": ""tenant1"", + ""instanceName"": ""instance 1"", + ""clientId"": ""one client"", + ""clientSecret"": ""one secret"", + ""resourceUrl"": ""http://www.myserver.com/data/v3/"", + ""oauthUrl"": ""http://www.myserver.com/connect/token"", + ""status"": ""Completed"" + },{ + ""id"": 2, + ""odsInstanceId"": 2, + ""tenantId"": 1, + ""tenantName"": ""tenant1"", + ""instanceName"": ""instance 2"", + ""clientId"": ""another client"", + ""clientSecret"": ""another secret"", + ""resourceUrl"": ""http://www.myserver.com/data/v3/"", + ""oauthUrl"": ""http://www.myserver.com/connect/token"", + ""status"": ""Completed"" + }]"; +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs index 28da0fefd..2f9697bf0 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs @@ -31,7 +31,7 @@ public class HealthCheckService(ILogger logger, IOdsApiCaller odsApiCaller) : IHealthCheckService { - private readonly ILogger _logger = logger; + private readonly ILogger _logger = logger; private readonly IAdminConsoleTenantsService _adminConsoleTenantsService = adminConsoleTenantsService; private readonly IGetInstancesQuery _getInstancesQuery = getInstancesQuery; private readonly IOdsApiCaller _odsApiCaller = odsApiCaller; diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs index 21edb933e..625bd757e 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -19,7 +19,7 @@ IConfiguration configuration { builder.Services.AddOptions(); builder.Services.Configure(configuration.GetSection("AppSettings")); - builder.Services.Configure(configuration.GetSection("OdsApiSettings")); + builder.Services.Configure(configuration.GetSection("HealthCheck:OdsApiSettings")); builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index f5b68bb11..65d92f1a7 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -219,9 +219,11 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) webApplicationBuilder.Services.AddTransient(); var adminConsoleIsEnabled = webApplicationBuilder.Configuration.GetValue("AppSettings:EnableAdminConsoleAPI"); - var healthCheckFrequency = webApplicationBuilder.Configuration.GetValue("AppSettings:HealthCheckFrequencyInMinutes"); + var healthCheckFrequency = webApplicationBuilder.Configuration.GetValue("HealthCheck:HealthCheckFrequencyInMinutes"); - if (adminConsoleIsEnabled) + // Schedule the health check job if the Admin Console API is enabled and + // the health check frequency is greater than zero. + if (adminConsoleIsEnabled && healthCheckFrequency > 0) { // Quartz.NET back end service webApplicationBuilder.Services.AddQuartz(q => diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 2efda569e..0df000009 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -12,30 +12,21 @@ "HealthCheckFrequencyInMinutes": 10, "IgnoresCertificateErrors": false }, - "AdminApiSettings": { - "AdminConsoleTenantsURL": "https://localhost/adminapi/adminconsole/tenants", - "AdminConsoleInstancesURL": "https://localhost/adminapi/adminconsole/instances", - "AdminConsoleHealthCheckURL": "https://localhost/adminapi/adminconsole/healthcheck", - "AccessTokenUrl": "https://localhost/adminapi/connect/token", - "Username": "", - "ClientId": "adminconsole-worker-client", - "ClientSecret": "7tpYh5eZtL0ct99cmfCXUY3q5o2KxUTU", - "GrantType": "client_credentials", - "Password": "", - "Scope": "edfi_admin_api/worker" - }, - "OdsApiSettings": { - "Endpoints": [ - "studentSpecialEducationProgramAssociations", - "studentDisciplineIncidentBehaviorAssociations", - "studentSchoolAssociations", - "studentSchoolAttendanceEvents", - "studentSectionAssociations", - "staffEducationOrganizationAssignmentAssociations", - "staffSectionAssociations", - "courseTranscripts", - "sections" - ] + "HealthCheck": { + "OdsApiSettings": { + "Endpoints": [ + "studentSpecialEducationProgramAssociations", + "studentDisciplineIncidentBehaviorAssociations", + "studentSchoolAssociations", + "studentSchoolAttendanceEvents", + "studentSectionAssociations", + "staffEducationOrganizationAssignmentAssociations", + "staffSectionAssociations", + "courseTranscripts", + "sections" + ] + }, + "HealthCheckFrequencyInMinutes": 0 }, "AdminConsoleSettings": { "ApplicationName": "Ed-Fi Health Check", From 7afb474e79930997e29bdee2656059594dc601cc Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 12:45:11 -0500 Subject: [PATCH 08/14] Update dev docker .md file with powershell script details --- docs/docker.md | 51 +++++++++++++++++++ .../setup-local-multi-tenants-docker.ps1 | 48 +++-------------- 2 files changed, 57 insertions(+), 42 deletions(-) rename setup-local.ps1 => eng/setup-local-multi-tenants-docker.ps1 (70%) diff --git a/docs/docker.md b/docs/docker.md index 9d3cc5c39..9aa9fede9 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -184,6 +184,57 @@ For local development and testing with keycloak, use `MultiTenant/compose-build- For testing pre-built binaries, use `MultiTenant/compose-build-binaries-multi-tenant.yml`. For testing pre-built binaries with keycloak, use `MultiTenant/compose-build-idp-binaries-multi-tenant.yml`. +### Multi-Tenant PowerShell Setup Script + +The project includes a PowerShell script to simplify multi-tenant Docker environment setup. The script automatically handles multiple compose files and provides comprehensive environment management. + +**Location:** `\eng\setup-local-multi-tenants-docker.ps1` + +**Features:** + +* Automatically runs multiple compose files together +* Validates prerequisites (Docker, compose files, environment) +* Provides health checks and status monitoring +* Supports build, start, stop, and log operations +* Uses the `.env` file for configuration + +**Usage Examples:** + +1. **Start multi-tenant environment:** + + ```powershell + .\eng\setup-local-multi-tenants-docker.ps1 + ``` + +2. **Build and start containers:** + + ```powershell + .\eng\setup-local-multi-tenants-docker.ps1 -Build + ``` + +3. **Start with log monitoring:** + + ```powershell + .\eng\setup-local-multi-tenants-docker.ps1 -Logs + ``` + +4. **Stop all containers:** + + ```powershell + .\eng\setup-local-multi-tenants-docker.ps1 -Down + ``` + +5. **Use custom environment file:** + + ```powershell + .\eng\setup-local-multi-tenants-docker.ps1 -EnvFile "custom.env" + ``` + +**Script combines these compose files:** + +* `MultiTenant/compose-build-dev-multi-tenant.yml` +* `MultiTenant/compose-build-ods-multi-tenant.yml` + ## Admin Api and Ed-Fi ODS / API docker containers Please refer [DOCKER DEPLOYMENT](https://techdocs.ed-fi.org/display/EDFITOOLS/Docker+Deployment) for diff --git a/setup-local.ps1 b/eng/setup-local-multi-tenants-docker.ps1 similarity index 70% rename from setup-local.ps1 rename to eng/setup-local-multi-tenants-docker.ps1 index ceee16678..2d9b9822f 100644 --- a/setup-local.ps1 +++ b/eng/setup-local-multi-tenants-docker.ps1 @@ -15,22 +15,19 @@ #> param( - [string]$EnvFile = ".env", + [string]$EnvFile = "..\Docker\Compose\pgsql\.env", [switch]$Down, [switch]$Build, [switch]$Logs ) -# Set error action preference $ErrorActionPreference = "Stop" -# Define paths $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$ComposeDir = Join-Path $ScriptDir "Docker\Compose\pgsql\MultiTenant" +$ComposeDir = Join-Path $ScriptDir "..\Docker\Compose\pgsql\MultiTenant" $OdsComposeFile = Join-Path $ComposeDir "compose-build-ods-multi-tenant.yml" $DevComposeFile = Join-Path $ComposeDir "compose-build-dev-multi-tenant.yml" -# Function to check if Docker is running function Test-DockerRunning { try { docker info | Out-Null @@ -41,7 +38,6 @@ function Test-DockerRunning { } } -# Function to check if file exists function Test-FileExists { param([string]$FilePath, [string]$Description) @@ -52,7 +48,6 @@ function Test-FileExists { Write-Host "✓ Found $Description" -ForegroundColor Green } -# Function to run Docker Compose command with multiple files function Invoke-MultiDockerCompose { param( [string[]]$ComposeFiles, @@ -70,8 +65,6 @@ function Invoke-MultiDockerCompose { $cmd += " -f `"$file`"" Write-Host "Using compose file: $file" -ForegroundColor Gray } - - # Add environment file if it exists if (Test-Path $EnvFile) { $cmd += " --env-file `"$EnvFile`"" Write-Host "Using environment file: $EnvFile" -ForegroundColor Gray @@ -95,35 +88,30 @@ function Invoke-MultiDockerCompose { } } -# Main execution try { Write-Host "=== Ed-Fi AdminAPI Multi-Tenant Docker Setup ===" -ForegroundColor Yellow Write-Host "Timestamp: $(Get-Date)" -ForegroundColor Gray - # Verify prerequisites Write-Host "Checking prerequisites..." -ForegroundColor Cyan if (-not (Test-DockerRunning)) { Write-Error "Docker is not running. Please start Docker Desktop and try again." exit 1 } - Write-Host "✓ Docker is running" -ForegroundColor Green + Write-Host "Docker is running" -ForegroundColor Green - # Check if docker-compose is available try { docker-compose --version | Out-Null - Write-Host "✓ Docker Compose is available" -ForegroundColor Green + Write-Host "Docker Compose is available" -ForegroundColor Green } catch { Write-Error "Docker Compose is not available. Please install Docker Compose." exit 1 } - # Verify compose files exist Test-FileExists -FilePath $DevComposeFile -Description "Dev compose file" Test-FileExists -FilePath $OdsComposeFile -Description "ODS compose file" - # Check environment file if (Test-Path $EnvFile) { Write-Host "Using environment file: $EnvFile" -ForegroundColor Green } else { @@ -135,7 +123,6 @@ try { $ComposeFiles = @($DevComposeFile, $OdsComposeFile) if ($Down) { - # Bring down containers Write-Host "Bringing down containers..." -ForegroundColor Red Invoke-MultiDockerCompose -ComposeFiles $ComposeFiles -Command "down --remove-orphans --volumes" -Description "Stopping all containers and removing volumes" @@ -143,37 +130,29 @@ try { Write-Host "All containers have been stopped and removed" -ForegroundColor Green } else { - # Start containers Write-Host "Starting multi-tenant environment..." -ForegroundColor Green - - # Build command $upCommand = "up -d" if ($Build) { $upCommand += " --build" Write-Host "Building containers..." -ForegroundColor Yellow } - # Start all containers together Invoke-MultiDockerCompose -ComposeFiles $ComposeFiles -Command $upCommand -Description "Starting multi-tenant containers" - # Wait for all services to be ready Write-Host "Waiting for all services to be ready..." -ForegroundColor Yellow Start-Sleep -Seconds 20 - # Show container status Write-Host "Container Status:" -ForegroundColor Cyan docker ps --filter "name=ed-fi" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - # Check health of containers - Write-Host "`Health Check:" -ForegroundColor Cyan + Write-Host "Health Check:" -ForegroundColor Cyan $unhealthyContainers = docker ps --filter "name=ed-fi" --filter "health=unhealthy" --format "{{.Names}}" if ($unhealthyContainers) { Write-Warning "Some containers are unhealthy: $unhealthyContainers" } else { - Write-Host "✓ All containers appear healthy" -ForegroundColor Green + Write-Host "All containers appear healthy" -ForegroundColor Green } - # Show logs if requested if ($Logs) { Write-Host "Container Logs:" -ForegroundColor Cyan Write-Host "Press Ctrl+C to stop following logs..." -ForegroundColor Gray @@ -192,24 +171,9 @@ try { } Write-Host "Multi-tenant environment setup completed!" -ForegroundColor Green - Write-Host "Access points:" -ForegroundColor Cyan - Write-Host " • AdminAPI: http://localhost:5001" -ForegroundColor White - Write-Host " • Swagger: http://localhost:5001/swagger" -ForegroundColor White - Write-Host " • Health: http://localhost:5001/health" -ForegroundColor White - Write-Host "Useful commands:" -ForegroundColor Cyan - Write-Host " • View logs: .\setup-local.ps1 -Logs" -ForegroundColor White - Write-Host " • Stop all: .\setup-local.ps1 -Down" -ForegroundColor White - Write-Host " • Rebuild: .\setup-local.ps1 -Build" -ForegroundColor White - Write-Host " • Status: docker ps --filter name=ed-fi" -ForegroundColor White } } catch { Write-Error "Script execution failed: $_" - Write-Host "Troubleshooting tips:" -ForegroundColor Yellow - Write-Host " 1. Ensure Docker Desktop is running" -ForegroundColor White - Write-Host " 2. Check if ports 5001, 5432 are available" -ForegroundColor White - Write-Host " 3. Verify .env file contains required variables" -ForegroundColor White - Write-Host " 4. Try running with -Build flag to rebuild containers" -ForegroundColor White - Write-Host " 5. Check container logs: docker-compose -f Dev -f ODS logs" -ForegroundColor White exit 1 } From a06e950f13eb5db67274fb4b7d69420107af76f7 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 13:07:08 -0500 Subject: [PATCH 09/14] Add documentation for health check integration --- .../HealthCheck/HealthCheckTrigger.cs | 2 +- docs/design/INTEGRATE-HEALTHCHECK-SERVICE | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 docs/design/INTEGRATE-HEALTHCHECK-SERVICE diff --git a/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs b/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs index 183afd8c1..9870f9475 100644 --- a/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs +++ b/Application/EdFi.Ods.AdminApi/Features/HealthCheck/HealthCheckTrigger.cs @@ -38,7 +38,7 @@ internal static async Task TriggerHealthCheck(ISchedulerFactory schedul await scheduler.AddJob(jobDetail, replace: true); } - // Fire-and-forget: schedule a one-time immediate trigger + // Schedule a one-time immediate trigger var trigger = TriggerBuilder.Create() .ForJob(jobKey) .WithIdentity($"ImmediateTrigger-{Guid.NewGuid()}") diff --git a/docs/design/INTEGRATE-HEALTHCHECK-SERVICE b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE new file mode 100644 index 000000000..093fce904 --- /dev/null +++ b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE @@ -0,0 +1,56 @@ +# Integrating EdFi.AdminConsole.HealthCheckService into EdFi.Ods.AdminApi with Quartz.NET + +## Overview + +This document describes the design and process for integrating the `EdFi.AdminConsole.HealthCheckService` into the `EdFi.Ods.AdminApi` application, leveraging Quartz.NET for scheduled and on-demand execution of health checks. + +--- + +## Goals + +* Enable scheduled health checks of ODS API instances via Quartz.NET. +* Allow on-demand triggering of health checks via an API endpoint. +* Ensure only one health check job runs at a time to prevent data conflicts. +* Centralize health check logic in `EdFi.AdminConsole.HealthCheckService`. + +--- + +## Architecture + +### Components + +* **HealthCheckService**: Core service that performs health checks across tenants and instances. +* **HealthCheckJob**: Quartz.NET job that invokes `HealthCheckService.Run()`. +* **Quartz.NET Scheduler**: Manages scheduled and ad-hoc job execution. +* **HealthCheckTrigger Endpoint**: API endpoint to trigger health checks on demand. + +--- + +## Process Flow + +### 1. Service Registration + +* Register `HealthCheckService` and its dependencies in the DI container (typically as `scoped` or `transient`). +* Register `HealthCheckJob` with Quartz.NET using `AddQuartz` and `AddQuartzHostedService`. + +### 2. Scheduling with Quartz.NET + +* Configure Quartz.NET to schedule `HealthCheckJob` at a configurable interval (e.g., every 10 minutes, using `HealthCheckFrequencyInMinutes` from configuration). +* Use the `[DisallowConcurrentExecution]` attribute on `HealthCheckJob` to prevent overlapping executions. + +### 3. On-Demand Triggering + +* Implement an API endpoint (e.g., `/healthcheck/trigger`) in `EdFi.Ods.AdminApi`. Note: Grouped with `adminconsole` endpoints for consistency. +* The endpoint uses `ISchedulerFactory` to schedule an immediate, one-time execution of `HealthCheckJob`. + +### 4. Concurrency Control + +* `[DisallowConcurrentExecution]` ensures only one instance of `HealthCheckJob` runs at a time, regardless of trigger source (scheduled or on-demand). + +--- + +## Configuration + +* **appsettings.json**: + * `HealthCheck:HealthCheckFrequencyInMinutes`: Controls the schedule interval. + * `AppSettings:EnableAdminConsoleAPI`: Enables or disables the health check API endpoint. From 105ffe9844cdae17f5be777667a1c48f875cd9ab Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 13:56:47 -0500 Subject: [PATCH 10/14] Fix merge conflicts --- Application/Directory.Packages.props | 24 +++++++++++ ...onsole.HealthCheckService.UnitTests.csproj | 12 +++--- .../Helpers/TestLoggerProvider.cs | 6 ++- ...dFi.AdminConsole.HealthCheckService.csproj | 40 +++++++++---------- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/Application/Directory.Packages.props b/Application/Directory.Packages.props index 513a86f14..4ae594879 100644 --- a/Application/Directory.Packages.props +++ b/Application/Directory.Packages.props @@ -79,5 +79,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj index 02de1553c..8d49bda82 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj @@ -8,15 +8,15 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs index 9fc06fd62..9c3362359 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs @@ -6,12 +6,14 @@ using Microsoft.Extensions.Logging; namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; + public class TestLoggerProvider : ILoggerProvider { - private readonly List _entries = new(); + private readonly List _entries = []; public IReadOnlyList Entries => _entries; public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, _entries); + public void Dispose() { } private class TestLogger(string category, List entries) : ILogger @@ -19,7 +21,7 @@ private class TestLogger(string category, List entries) : ILogger private readonly List _entries = entries; private readonly string _category = category; - public IDisposable BeginScope(TState state) => NullScope.Instance; + public IDisposable? BeginScope(TState state) => NullScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log( diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj index 8c4fc49f7..cbf813fa0 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj +++ b/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj @@ -7,31 +7,31 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + From f4c6f33e5717483417b319425112735e2dabadc3 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 14:55:37 -0500 Subject: [PATCH 11/14] Fix build errors --- .github/workflows/api-e2e-mssql-multitenant.yml | 3 +++ .github/workflows/api-e2e-mssql-singletenant.yml | 3 +++ .github/workflows/api-e2e-pgsql-multitenant.yml | 3 +++ .github/workflows/api-e2e-pgsql-singletenant.yml | 1 + Application/EdFi.Ods.AdminApi/appsettings.json | 2 +- Docker/dev.mssql.Dockerfile | 3 +++ 6 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api-e2e-mssql-multitenant.yml b/.github/workflows/api-e2e-mssql-multitenant.yml index a5c8109d5..cf1fd22df 100644 --- a/.github/workflows/api-e2e-mssql-multitenant.yml +++ b/.github/workflows/api-e2e-mssql-multitenant.yml @@ -46,6 +46,9 @@ jobs: - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + - name: Copy health check service folder to docker context + run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-mssql-singletenant.yml b/.github/workflows/api-e2e-mssql-singletenant.yml index 3964ceed0..541b4790a 100644 --- a/.github/workflows/api-e2e-mssql-singletenant.yml +++ b/.github/workflows/api-e2e-mssql-singletenant.yml @@ -46,6 +46,9 @@ jobs: - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + - name: Copy health check service folder to docker context + run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-multitenant.yml b/.github/workflows/api-e2e-pgsql-multitenant.yml index fadc070a5..17aa2a5ef 100644 --- a/.github/workflows/api-e2e-pgsql-multitenant.yml +++ b/.github/workflows/api-e2e-pgsql-multitenant.yml @@ -46,6 +46,9 @@ jobs: - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + - name: Copy health check service folder to docker context + run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-singletenant.yml b/.github/workflows/api-e2e-pgsql-singletenant.yml index 18c2b0588..bf391d9f4 100644 --- a/.github/workflows/api-e2e-pgsql-singletenant.yml +++ b/.github/workflows/api-e2e-pgsql-singletenant.yml @@ -41,6 +41,7 @@ jobs: cp -r ../EdFi.Ods.AdminApi ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application + cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 0df000009..a8ef84321 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -8,7 +8,7 @@ "MultiTenancy": false, "PreventDuplicateApplications": false, "EnableAdminConsoleAPI": false, - "EnableApplicationResetEndpoint": true + "EnableApplicationResetEndpoint": true, "HealthCheckFrequencyInMinutes": 10, "IgnoresCertificateErrors": false }, diff --git a/Docker/dev.mssql.Dockerfile b/Docker/dev.mssql.Dockerfile index 27e78554a..c80225191 100644 --- a/Docker/dev.mssql.Dockerfile +++ b/Docker/dev.mssql.Dockerfile @@ -24,6 +24,9 @@ COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminAp COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ +COPY --from=assets ./Application/NuGet.Config EdFi.AdminConsole.HealthCheckService/ +COPY --from=assets ./Application/EdFi.AdminConsole.HealthCheckService EdFi.AdminConsole.HealthCheckService/ + WORKDIR /source/EdFi.Ods.AdminApi RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT RUN dotnet restore && dotnet build -c Release From 465ab30e58f588f58ab35141ae455699245fcf57 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 15:29:27 -0500 Subject: [PATCH 12/14] Add coverlet collector --- .../EdFi.AdminConsole.HealthCheckService.UnitTests.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj index 8d49bda82..188a300d9 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj @@ -8,11 +8,15 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 81d951a433cbd2a0c80e8bdbbbabed06e8d0aca9 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Mon, 4 Aug 2025 16:44:49 -0500 Subject: [PATCH 13/14] Add app http client unit tests --- .../Infrastructure/AppHttpClientTests.cs | 143 ++++++++++++++++++ .../Logging/DefaultLogger.cs | 22 --- 2 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs new file mode 100644 index 000000000..b23ce9ccc --- /dev/null +++ b/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Net; +using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Infrastructure; + +[TestFixture] +public class AppHttpClientTests +{ + private AppSettings _settings; + private IOptions _options; + private ILogger _logger; + + [SetUp] + public void SetUp() + { + _settings = new AppSettings { MaxRetryAttempts = 2 }; + _options = Options.Create(_settings); + _logger = A.Fake>(); + } + + private static HttpClient CreateHttpClient(HttpResponseMessage responseMessage, out FakeHttpMessageHandler handler) + { + handler = new FakeHttpMessageHandler(responseMessage); + return new HttpClient(handler); + } + + [Test] + public async Task SendAsync_StringContent_ReturnsApiResponse_OnSuccess() + { + // Arrange + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("success") + }; + var httpClient = CreateHttpClient(responseMessage, out _); + var sut = new AppHttpClient(httpClient, _logger, _options); + + // Act + var result = await sut.SendAsync("http://test", HttpMethod.Get, new StringContent(""), null); + + // Assert + result.StatusCode.ShouldBe(HttpStatusCode.OK); + result.Content.ShouldBe("success"); + } + + [Test] + public async Task SendAsync_StringContent_RetriesOnTransientFailure() + { + // Arrange + int callCount = 0; + var handler = new FakeHttpMessageHandler(() => + { + callCount++; + if (callCount == 1) + return new HttpResponseMessage(HttpStatusCode.RequestTimeout); + return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("recovered") }; + }); + + var httpClient = new HttpClient(handler); + var sut = new AppHttpClient(httpClient, _logger, _options); + + // Act + var result = await sut.SendAsync("http://test", HttpMethod.Get, new StringContent(""), null); + + // Assert + result.StatusCode.ShouldBe(HttpStatusCode.OK); + result.Content.ShouldBe("recovered"); + callCount.ShouldBe(2); + } + + [Test] + public async Task SendAsync_StringContent_LogsWarning_OnNonOkStatus() + { + // Arrange + var responseMessage = new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent("bad request") + }; + var httpClient = CreateHttpClient(responseMessage, out _); + var sut = new AppHttpClient(httpClient, _logger, _options); + + // Act + var result = await sut.SendAsync("http://test", HttpMethod.Post, new StringContent(""), null); + + // Assert + result.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + result.Content.ShouldBe("bad request"); + } + + [Test] + public async Task SendAsync_FormUrlEncodedContent_ReturnsApiResponse_OnSuccess() + { + // Arrange + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("form success") + }; + var httpClient = CreateHttpClient(responseMessage, out _); + var sut = new AppHttpClient(httpClient, _logger, _options); + + // Act + var result = await sut.SendAsync("http://test", HttpMethod.Post, new FormUrlEncodedContent([]), null); + + // Assert + result.StatusCode.ShouldBe(HttpStatusCode.OK); + result.Content.ShouldBe("form success"); + } + + private class FakeHttpMessageHandler : HttpMessageHandler + { + private readonly Func? _responseFactory; + private readonly HttpResponseMessage? _staticResponse; + + public FakeHttpMessageHandler(HttpResponseMessage staticResponse) + { + _staticResponse = staticResponse; + } + + public FakeHttpMessageHandler(Func? responseFactory) + { + _responseFactory = responseFactory; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (_responseFactory != null) + return Task.FromResult(_responseFactory()); + if (_staticResponse != null) + return Task.FromResult(_staticResponse); + throw new InvalidOperationException("No response configured for FakeHttpMessageHandler."); + } + } +} diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs b/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs deleted file mode 100644 index a43d3b1ba..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/Logging/DefaultLogger.cs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using Serilog; -using Serilog.Core; -using Serilog.Events; - -namespace EdFi.AdminConsole.HealthCheckService.Logging; - -public static class DefaultLogger -{ - public static Logger Build() - { - return new LoggerConfiguration() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Console(outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss,fff}] [{Level:u4}] [{SourceContext}] {Message} {Exception} {NewLine}") - .CreateLogger(); - } -} \ No newline at end of file From c0e2e0deb8c68018212201a467dbc412aa1f0462 Mon Sep 17 00:00:00 2001 From: CSR2017 Date: Thu, 7 Aug 2025 17:09:39 -0500 Subject: [PATCH 14/14] Rename the project to EdFi.Ods.AdminApi.HealthCheck --- .../workflows/api-e2e-mssql-multitenant.yml | 2 +- .../workflows/api-e2e-mssql-singletenant.yml | 2 +- .../workflows/api-e2e-pgsql-multitenant.yml | 2 +- .../workflows/api-e2e-pgsql-singletenant.yml | 2 +- Application/Ed-Fi-ODS-AdminApi.sln | 36 +++++++++---------- .../logging.json | 22 ------------ ...Ods.AdminApi.HealthCheck.UnitTests.csproj} | 2 +- .../Features/OdsApi/OdsApiCallerTests.cs | 8 ++--- .../Features/OdsApi/OdsApiClientTests.cs | 8 ++--- .../HealthCheckServiceTests.cs | 10 +++--- .../Helpers/InstanceValidatorTests.cs | 6 ++-- .../Helpers/JsonBuilderTests.cs | 6 ++-- .../Helpers/TestLoggerProvider.cs | 2 +- .../Infrastructure/AppHttpClientTests.cs | 5 +-- .../Testing.cs | 6 ++-- .../AppSettings.cs | 2 +- ...dFi.AdminConsole.HealthCheckService.nuspec | 0 .../EdFi.Ods.AdminApi.HealthCheck.csproj} | 17 ++------- .../Extensions/HttpStatusCode.cs | 2 +- .../Features/AdminApi/AdminConsoleInstance.cs | 2 +- .../OdsApi/AppSettingsOdsApiEndpoints.cs | 2 +- .../Features/OdsApi/OdsApiCaller.cs | 6 ++-- .../Features/OdsApi/OdsApiClient.cs | 4 +-- .../OdsApi/OdsApiEndpointNameCount.cs | 2 +- .../HealthCheckService.cs | 8 ++--- .../Helpers/Constants.cs | 2 +- .../Helpers/InstanceValidator.cs | 4 +-- .../Helpers/JsonBuilder.cs | 4 +-- .../Infrastructure/ApiResponse.cs | 2 +- .../Infrastructure/AppHttpClient.cs | 6 ++-- .../HttpRequestMessageBuilder.cs | 2 +- .../OdsApiSettings.cs | 2 +- .../EdFi.Ods.AdminApi.csproj | 4 +-- .../BackgroundJobs/HealthCheckJob.cs | 2 +- .../HealthCheckServiceExtension.cs | 6 ++-- .../EdFi.Ods.AdminApi/appsettings.json | 1 - Docker/dev.mssql.Dockerfile | 4 +-- Docker/dev.pgsql.Dockerfile | 4 +-- docs/design/INTEGRATE-HEALTHCHECK-SERVICE | 4 +-- 39 files changed, 88 insertions(+), 123 deletions(-) delete mode 100644 Application/EdFi.AdminConsole.HealthCheckService/logging.json rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj => EdFi.Ods.AdminApi.HealthCheck.UnitTests/EdFi.Ods.AdminApi.HealthCheck.UnitTests.csproj} (89%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Features/OdsApi/OdsApiCallerTests.cs (91%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Features/OdsApi/OdsApiClientTests.cs (93%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/HealthCheckServiceTests.cs (98%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Helpers/InstanceValidatorTests.cs (93%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Helpers/JsonBuilderTests.cs (93%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Helpers/TestLoggerProvider.cs (96%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Infrastructure/AppHttpClientTests.cs (96%) rename Application/{EdFi.AdminConsole.HealthCheckService.UnitTests => EdFi.Ods.AdminApi.HealthCheck.UnitTests}/Testing.cs (95%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/AppSettings.cs (88%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/EdFi.AdminConsole.HealthCheckService.nuspec (100%) rename Application/{EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj => EdFi.Ods.AdminApi.HealthCheck/EdFi.Ods.AdminApi.HealthCheck.csproj} (69%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Extensions/HttpStatusCode.cs (92%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Features/AdminApi/AdminConsoleInstance.cs (93%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Features/OdsApi/AppSettingsOdsApiEndpoints.cs (93%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Features/OdsApi/OdsApiCaller.cs (92%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Features/OdsApi/OdsApiClient.cs (96%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Features/OdsApi/OdsApiEndpointNameCount.cs (88%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/HealthCheckService.cs (96%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Helpers/Constants.cs (92%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Helpers/InstanceValidator.cs (92%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Helpers/JsonBuilder.cs (89%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Infrastructure/ApiResponse.cs (92%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Infrastructure/AppHttpClient.cs (96%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/Infrastructure/HttpRequestMessageBuilder.cs (95%) rename Application/{EdFi.AdminConsole.HealthCheckService => EdFi.Ods.AdminApi.HealthCheck}/OdsApiSettings.cs (90%) diff --git a/.github/workflows/api-e2e-mssql-multitenant.yml b/.github/workflows/api-e2e-mssql-multitenant.yml index cf1fd22df..d1f93475a 100644 --- a/.github/workflows/api-e2e-mssql-multitenant.yml +++ b/.github/workflows/api-e2e-mssql-multitenant.yml @@ -47,7 +47,7 @@ jobs: run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application - name: Copy health check service folder to docker context - run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + run: cp -r ../EdFi.Ods.AdminApi.HealthCheck ../../Docker/Application - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-mssql-singletenant.yml b/.github/workflows/api-e2e-mssql-singletenant.yml index 541b4790a..2e06e8ff8 100644 --- a/.github/workflows/api-e2e-mssql-singletenant.yml +++ b/.github/workflows/api-e2e-mssql-singletenant.yml @@ -47,7 +47,7 @@ jobs: run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application - name: Copy health check service folder to docker context - run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + run: cp -r ../EdFi.Ods.AdminApi.HealthCheck ../../Docker/Application - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-multitenant.yml b/.github/workflows/api-e2e-pgsql-multitenant.yml index 17aa2a5ef..ccf512bae 100644 --- a/.github/workflows/api-e2e-pgsql-multitenant.yml +++ b/.github/workflows/api-e2e-pgsql-multitenant.yml @@ -47,7 +47,7 @@ jobs: run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application - name: Copy health check service folder to docker context - run: cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + run: cp -r ../EdFi.Ods.AdminApi.HealthCheck ../../Docker/Application - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-singletenant.yml b/.github/workflows/api-e2e-pgsql-singletenant.yml index bf391d9f4..4ce890573 100644 --- a/.github/workflows/api-e2e-pgsql-singletenant.yml +++ b/.github/workflows/api-e2e-pgsql-singletenant.yml @@ -41,7 +41,7 @@ jobs: cp -r ../EdFi.Ods.AdminApi ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application - cp -r ../EdFi.AdminConsole.HealthCheckService ../../Docker/Application + cp -r ../EdFi.Ods.AdminApi.HealthCheck ../../Docker/Application - name: Copy nuget config to docker context run: cp ../NuGet.Config ../../Docker/Application diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index f316fbd1d..d22977502 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -30,9 +30,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.AdminConsole.UnitTests", "EdFi.Ods.AdminApi.AdminConsole.UnitTests\EdFi.Ods.AdminApi.AdminConsole.UnitTests.csproj", "{7C919128-B651-4756-8625-A8F7882FA9A4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.AdminConsole.HealthCheckService", "EdFi.AdminConsole.HealthCheckService\EdFi.AdminConsole.HealthCheckService.csproj", "{89457FDC-0611-4703-DA46-DEDD38C5FE22}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.HealthCheck", "EdFi.Ods.AdminApi.HealthCheck\EdFi.Ods.AdminApi.HealthCheck.csproj", "{243D1BB9-7E56-7439-D4DF-438EF9A8692F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.AdminConsole.HealthCheckService.UnitTests", "EdFi.AdminConsole.HealthCheckService.UnitTests\EdFi.AdminConsole.HealthCheckService.UnitTests.csproj", "{16AC7821-B294-1CEF-A93E-3164AC9CBCE9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.HealthCheck.UnitTests", "EdFi.Ods.AdminApi.HealthCheck.UnitTests\EdFi.Ods.AdminApi.HealthCheck.UnitTests.csproj", "{9CF7B5C5-92F6-A980-A213-71B14729F0A0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -106,22 +106,22 @@ Global {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|Any CPU.Build.0 = Release|Any CPU {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|x64.ActiveCfg = Release|Any CPU {7C919128-B651-4756-8625-A8F7882FA9A4}.Release|x64.Build.0 = Release|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|x64.ActiveCfg = Debug|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Debug|x64.Build.0 = Debug|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|Any CPU.Build.0 = Release|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.ActiveCfg = Release|Any CPU - {89457FDC-0611-4703-DA46-DEDD38C5FE22}.Release|x64.Build.0 = Release|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|x64.ActiveCfg = Debug|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Debug|x64.Build.0 = Debug|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|Any CPU.Build.0 = Release|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|x64.ActiveCfg = Release|Any CPU - {16AC7821-B294-1CEF-A93E-3164AC9CBCE9}.Release|x64.Build.0 = Release|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Debug|x64.ActiveCfg = Debug|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Debug|x64.Build.0 = Debug|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Release|Any CPU.Build.0 = Release|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Release|x64.ActiveCfg = Release|Any CPU + {243D1BB9-7E56-7439-D4DF-438EF9A8692F}.Release|x64.Build.0 = Release|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Debug|x64.Build.0 = Debug|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Release|Any CPU.Build.0 = Release|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Release|x64.ActiveCfg = Release|Any CPU + {9CF7B5C5-92F6-A980-A213-71B14729F0A0}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Application/EdFi.AdminConsole.HealthCheckService/logging.json b/Application/EdFi.AdminConsole.HealthCheckService/logging.json deleted file mode 100644 index a03245d9c..000000000 --- a/Application/EdFi.AdminConsole.HealthCheckService/logging.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "WebOptimizer": "Warning" - } - }, - "Using": [ "Serilog.Sinks.Console" ], - "WriteTo": { - "ConsoleSink": { - "Name": "Console", - "Args": { - "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss,fff}] [{LevelShortName}] [{SourceContext}] {Message} {Exception} {NewLine}" - } - } - } - - } -} diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/EdFi.Ods.AdminApi.HealthCheck.UnitTests.csproj similarity index 89% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/EdFi.Ods.AdminApi.HealthCheck.UnitTests.csproj index 188a300d9..dd9f8ec2c 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/EdFi.AdminConsole.HealthCheckService.UnitTests.csproj +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/EdFi.Ods.AdminApi.HealthCheck.UnitTests.csproj @@ -24,7 +24,7 @@ - + diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiCallerTests.cs similarity index 91% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiCallerTests.cs index a721fdbc9..734ab3fa0 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiCallerTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiCallerTests.cs @@ -4,14 +4,14 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Net; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; using FakeItEasy; using NUnit.Framework; using Shouldly; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Features.OdsApi; public class Given_an_ods_api { diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiClientTests.cs similarity index 93% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiClientTests.cs index cb816507f..27c65e8d3 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Features/OdsApi/OdsApiClientTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Features/OdsApi/OdsApiClientTests.cs @@ -6,15 +6,15 @@ using System.Net; using System.Net.Http.Headers; using System.Text; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Infrastructure; using FakeItEasy; using Microsoft.Extensions.Logging; using NUnit.Framework; using Shouldly; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Features.OdsApi; public class Given_an_ods_environment_with_single_tenant { diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/HealthCheckServiceTests.cs similarity index 98% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/HealthCheckServiceTests.cs index 2e9acf1d5..6b5e27999 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/HealthCheckServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/HealthCheckServiceTests.cs @@ -5,8 +5,6 @@ using System.Dynamic; using System.Text; -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; using EdFi.Ods.AdminApi.AdminConsole.Features.WorkerInstances; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; @@ -18,9 +16,11 @@ using System.Text.Json; using NUnit.Framework; using Shouldly; -using EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.UnitTests.Helpers; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests; public class Given_a_health_check_service { @@ -60,7 +60,7 @@ public void SetUp() var instances = CreateTestInstances(); var healthCheckData = CreateTestHealthCheckData(); - var savedHealthCheck = new HealthCheck + var savedHealthCheck = new AdminConsole.Infrastructure.DataAccess.Models.HealthCheck { DocId = 1, InstanceId = 1, diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/InstanceValidatorTests.cs similarity index 93% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/InstanceValidatorTests.cs index a6f0fd4e6..8f3123fa6 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/InstanceValidatorTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/InstanceValidatorTests.cs @@ -3,14 +3,14 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; using FakeItEasy; using Microsoft.Extensions.Logging; using NUnit.Framework; using Shouldly; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Helpers; public class Given_an_instance_returned_from_AdminApi { diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/JsonBuilderTests.cs similarity index 93% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/JsonBuilderTests.cs index b37d6fc47..3c17fd37c 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/JsonBuilderTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/JsonBuilderTests.cs @@ -3,13 +3,13 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; using Newtonsoft.Json.Linq; using NUnit.Framework; using Shouldly; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Helpers; public class Given_a_set_of_healthCheck_data { diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/TestLoggerProvider.cs similarity index 96% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/TestLoggerProvider.cs index 9c3362359..a7697fd31 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Helpers/TestLoggerProvider.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Helpers/TestLoggerProvider.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Helpers; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Helpers; public class TestLoggerProvider : ILoggerProvider { diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Infrastructure/AppHttpClientTests.cs similarity index 96% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Infrastructure/AppHttpClientTests.cs index b23ce9ccc..64267c0f1 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Infrastructure/AppHttpClientTests.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Infrastructure/AppHttpClientTests.cs @@ -4,14 +4,15 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Net; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck; using FakeItEasy; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NUnit.Framework; using Shouldly; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests.Infrastructure; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests.Infrastructure; [TestFixture] public class AppHttpClientTests diff --git a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Testing.cs similarity index 95% rename from Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Testing.cs index 480ffcecd..e3db80477 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService.UnitTests/Testing.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck.UnitTests/Testing.cs @@ -3,11 +3,11 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; using Microsoft.Extensions.Options; -namespace EdFi.AdminConsole.HealthCheckService.UnitTests; +namespace EdFi.Ods.AdminApi.HealthCheck.UnitTests; public class Testing { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/AppSettings.cs similarity index 88% rename from Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/AppSettings.cs index 49af5fab3..d02870003 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/AppSettings.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/AppSettings.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService; +namespace EdFi.Ods.AdminApi.HealthCheck; public sealed class AppSettings { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec b/Application/EdFi.Ods.AdminApi.HealthCheck/EdFi.AdminConsole.HealthCheckService.nuspec similarity index 100% rename from Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.nuspec rename to Application/EdFi.Ods.AdminApi.HealthCheck/EdFi.AdminConsole.HealthCheckService.nuspec diff --git a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj b/Application/EdFi.Ods.AdminApi.HealthCheck/EdFi.Ods.AdminApi.HealthCheck.csproj similarity index 69% rename from Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj rename to Application/EdFi.Ods.AdminApi.HealthCheck/EdFi.Ods.AdminApi.HealthCheck.csproj index cbf813fa0..4fa494056 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/EdFi.AdminConsole.HealthCheckService.csproj +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/EdFi.Ods.AdminApi.HealthCheck.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -20,14 +20,7 @@ - - - - - - - - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -38,10 +31,4 @@ - - - Always - - - diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Extensions/HttpStatusCode.cs similarity index 92% rename from Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Extensions/HttpStatusCode.cs index 6cb2843dd..53f0af5d6 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Extensions/HttpStatusCode.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Extensions/HttpStatusCode.cs @@ -5,7 +5,7 @@ using System.Net; -namespace EdFi.AdminConsole.HealthCheckService.Extensions; +namespace EdFi.Ods.AdminApi.HealthCheck.Extensions; public static class HttpStatusCodeExtensions { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/AdminApi/AdminConsoleInstance.cs similarity index 93% rename from Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Features/AdminApi/AdminConsoleInstance.cs index f84aa6cad..984081551 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/AdminApi/AdminConsoleInstance.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/AdminApi/AdminConsoleInstance.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +namespace EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; public class AdminConsoleInstance { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/AppSettingsOdsApiEndpoints.cs similarity index 93% rename from Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/AppSettingsOdsApiEndpoints.cs index f4d789958..7940c4a22 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/AppSettingsOdsApiEndpoints.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/AppSettingsOdsApiEndpoints.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Options; using System.Collections; -namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; public interface IAppSettingsOdsApiEndpoints : IEnumerable { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiCaller.cs similarity index 92% rename from Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiCaller.cs index fb8b74737..150c00ebc 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiCaller.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiCaller.cs @@ -3,10 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; -namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; public interface IOdsApiCaller { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiClient.cs similarity index 96% rename from Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiClient.cs index 83cf149c7..250556122 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiClient.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiClient.cs @@ -6,12 +6,12 @@ using System.Net; using System.Net.Http.Headers; using System.Text; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; +using EdFi.Ods.AdminApi.HealthCheck.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; -namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; public interface IOdsApiClient { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiEndpointNameCount.cs similarity index 88% rename from Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiEndpointNameCount.cs index 74f4d3017..6680986e3 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Features/OdsApi/OdsApiEndpointNameCount.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Features/OdsApi/OdsApiEndpointNameCount.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +namespace EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; public class OdsApiEndpointNameCount { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/HealthCheckService.cs similarity index 96% rename from Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/HealthCheckService.cs index 2f9697bf0..04a9bf706 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/HealthCheckService.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/HealthCheckService.cs @@ -3,8 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Helpers; +using EdFi.Ods.AdminApi.HealthCheck.Helpers; using Microsoft.Extensions.Logging; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; @@ -15,9 +14,10 @@ using Newtonsoft.Json; using System.Text; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; -namespace EdFi.AdminConsole.HealthCheckService; +namespace EdFi.Ods.AdminApi.HealthCheck; public interface IHealthCheckService { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/Constants.cs similarity index 92% rename from Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/Constants.cs index 4c047b646..5173ec24e 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/Constants.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/Constants.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService.Helpers +namespace EdFi.Ods.AdminApi.HealthCheck.Helpers { public struct Constants { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/InstanceValidator.cs similarity index 92% rename from Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/InstanceValidator.cs index bd1a72610..9225fb244 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/InstanceValidator.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/InstanceValidator.cs @@ -3,10 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.AdminApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.AdminApi; using Microsoft.Extensions.Logging; -namespace EdFi.AdminConsole.HealthCheckService.Helpers; +namespace EdFi.Ods.AdminApi.HealthCheck.Helpers; public static class InstanceValidator { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/JsonBuilder.cs similarity index 89% rename from Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/JsonBuilder.cs index 091cab99b..d62bb3938 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Helpers/JsonBuilder.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Helpers/JsonBuilder.cs @@ -3,10 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; using System.Text.Json.Nodes; -namespace EdFi.AdminConsole.HealthCheckService.Helpers; +namespace EdFi.Ods.AdminApi.HealthCheck.Helpers; public static class JsonBuilder { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/ApiResponse.cs similarity index 92% rename from Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/ApiResponse.cs index 91997fc47..d2186ea87 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/ApiResponse.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/ApiResponse.cs @@ -6,7 +6,7 @@ using System.Net; using System.Net.Http.Headers; -namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; +namespace EdFi.Ods.AdminApi.HealthCheck.Infrastructure; public class ApiResponse { public HttpStatusCode StatusCode { get; } diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/AppHttpClient.cs similarity index 96% rename from Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/AppHttpClient.cs index 4162f6057..7626bfaf7 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/AppHttpClient.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/AppHttpClient.cs @@ -5,14 +5,14 @@ using System.Net; using System.Net.Http.Headers; -using EdFi.AdminConsole.HealthCheckService.Extensions; +using EdFi.Ods.AdminApi.HealthCheck.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Polly; using Polly.Contrib.WaitAndRetry; -using Constants = EdFi.AdminConsole.HealthCheckService.Helpers.Constants; +using Constants = EdFi.Ods.AdminApi.HealthCheck.Helpers.Constants; -namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; +namespace EdFi.Ods.AdminApi.HealthCheck.Infrastructure; public interface IAppHttpClient { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/HttpRequestMessageBuilder.cs similarity index 95% rename from Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/HttpRequestMessageBuilder.cs index 5d5fce4de..0fec31c58 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/Infrastructure/HttpRequestMessageBuilder.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/Infrastructure/HttpRequestMessageBuilder.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService.Infrastructure; +namespace EdFi.Ods.AdminApi.HealthCheck.Infrastructure; public interface IHttpRequestMessageBuilder { diff --git a/Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs b/Application/EdFi.Ods.AdminApi.HealthCheck/OdsApiSettings.cs similarity index 90% rename from Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs rename to Application/EdFi.Ods.AdminApi.HealthCheck/OdsApiSettings.cs index d6f92c4c0..58ce1c15a 100644 --- a/Application/EdFi.AdminConsole.HealthCheckService/OdsApiSettings.cs +++ b/Application/EdFi.Ods.AdminApi.HealthCheck/OdsApiSettings.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -namespace EdFi.AdminConsole.HealthCheckService; +namespace EdFi.Ods.AdminApi.HealthCheck; public interface IOdsApiSettings { diff --git a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj index af3ba3b6a..1148b96a2 100644 --- a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj +++ b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj @@ -1,4 +1,4 @@ - + net8.0 enable @@ -54,8 +54,8 @@ - + diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs index 5ff556164..15954384b 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckJob.cs @@ -3,7 +3,7 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService; +using EdFi.Ods.AdminApi.HealthCheck; using Quartz; namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs index 625bd757e..7c9c6fb55 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/BackgroundJobs/HealthCheckServiceExtension.cs @@ -3,10 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using EdFi.AdminConsole.HealthCheckService; -using EdFi.AdminConsole.HealthCheckService.Features.OdsApi; -using EdFi.AdminConsole.HealthCheckService.Infrastructure; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; +using EdFi.Ods.AdminApi.HealthCheck; +using EdFi.Ods.AdminApi.HealthCheck.Features.OdsApi; +using EdFi.Ods.AdminApi.HealthCheck.Infrastructure; namespace EdFi.Ods.AdminApi.Infrastructure.BackgroundJobs; diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index a8ef84321..f43dc58e3 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -9,7 +9,6 @@ "PreventDuplicateApplications": false, "EnableAdminConsoleAPI": false, "EnableApplicationResetEndpoint": true, - "HealthCheckFrequencyInMinutes": 10, "IgnoresCertificateErrors": false }, "HealthCheck": { diff --git a/Docker/dev.mssql.Dockerfile b/Docker/dev.mssql.Dockerfile index c80225191..faa1ed5c5 100644 --- a/Docker/dev.mssql.Dockerfile +++ b/Docker/dev.mssql.Dockerfile @@ -24,8 +24,8 @@ COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminAp COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ -COPY --from=assets ./Application/NuGet.Config EdFi.AdminConsole.HealthCheckService/ -COPY --from=assets ./Application/EdFi.AdminConsole.HealthCheckService EdFi.AdminConsole.HealthCheckService/ +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.HealthCheck/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.HealthCheck EdFi.Ods.AdminApi.HealthCheck/ WORKDIR /source/EdFi.Ods.AdminApi RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT diff --git a/Docker/dev.pgsql.Dockerfile b/Docker/dev.pgsql.Dockerfile index 8a3efcbf4..74275831d 100644 --- a/Docker/dev.pgsql.Dockerfile +++ b/Docker/dev.pgsql.Dockerfile @@ -24,8 +24,8 @@ COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminAp COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ -COPY --from=assets ./Application/NuGet.Config EdFi.AdminConsole.HealthCheckService/ -COPY --from=assets ./Application/EdFi.AdminConsole.HealthCheckService EdFi.AdminConsole.HealthCheckService/ +COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.HealthCheck/ +COPY --from=assets ./Application/EdFi.Ods.AdminApi.HealthCheck EdFi.Ods.AdminApi.HealthCheck/ WORKDIR /source/EdFi.Ods.AdminApi RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT diff --git a/docs/design/INTEGRATE-HEALTHCHECK-SERVICE b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE index 093fce904..5e4c4c917 100644 --- a/docs/design/INTEGRATE-HEALTHCHECK-SERVICE +++ b/docs/design/INTEGRATE-HEALTHCHECK-SERVICE @@ -11,7 +11,7 @@ This document describes the design and process for integrating the `EdFi.AdminCo * Enable scheduled health checks of ODS API instances via Quartz.NET. * Allow on-demand triggering of health checks via an API endpoint. * Ensure only one health check job runs at a time to prevent data conflicts. -* Centralize health check logic in `EdFi.AdminConsole.HealthCheckService`. +* Centralize health check logic in `EdFi.Ods.AdminApi.HealthCheck`. --- @@ -19,7 +19,7 @@ This document describes the design and process for integrating the `EdFi.AdminCo ### Components -* **HealthCheckService**: Core service that performs health checks across tenants and instances. +* **HealthCheckService**: Service class that performs health checks across tenants and instances. * **HealthCheckJob**: Quartz.NET job that invokes `HealthCheckService.Run()`. * **Quartz.NET Scheduler**: Manages scheduled and ad-hoc job execution. * **HealthCheckTrigger Endpoint**: API endpoint to trigger health checks on demand.