Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62ba5e9487 | |||
| 136614be94 | |||
| a912bffad5 |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="dohertj2-gitea" value="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json" />
|
||||
</packageSources>
|
||||
<!-- nuget.org serves everything; the Gitea feed serves only the ZB.MOM.WW.* shared libs.
|
||||
Credentials are NOT committed: they are provided per-developer at the user level. -->
|
||||
<packageSourceMapping>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="dohertj2-gitea">
|
||||
<package pattern="ZB.MOM.WW.Health" />
|
||||
<package pattern="ZB.MOM.WW.Health.*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Readiness probe: verifies the SQLite authentication store is reachable. The gateway
|
||||
/// authenticates every gRPC call against this store, so its reachability gates readiness.
|
||||
/// </summary>
|
||||
public sealed class AuthStoreHealthCheck : IHealthCheck
|
||||
{
|
||||
private readonly AuthSqliteConnectionFactory _connectionFactory;
|
||||
|
||||
public AuthStoreHealthCheck(AuthSqliteConnectionFactory connectionFactory) =>
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||
HealthCheckContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using SqliteConnection connection =
|
||||
await _connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using SqliteCommand command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT 1;";
|
||||
await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
|
||||
return HealthCheckResult.Healthy("Auth store is reachable.");
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy("Auth store is unreachable.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Configuration;
|
||||
using ZB.MOM.WW.Health;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
@@ -63,7 +64,11 @@ public static class GatewayApplication
|
||||
builder.Services.AddGatewayConfiguration();
|
||||
builder.Services.AddSqliteAuthStore();
|
||||
builder.Services.AddGatewayGrpcAuthorization();
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddTypeActivatedCheck<AuthStoreHealthCheck>(
|
||||
"auth-store",
|
||||
failureStatus: null,
|
||||
tags: new[] { ZbHealthTags.Ready });
|
||||
builder.Services.AddSingleton<GatewayMetrics>();
|
||||
builder.Services.AddSingleton<MxAccessGrpcMapper>();
|
||||
builder.Services.AddSingleton<MxAccessGrpcRequestValidator>();
|
||||
@@ -169,13 +174,7 @@ public static class GatewayApplication
|
||||
{
|
||||
endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath());
|
||||
|
||||
endpoints.MapGet(
|
||||
"/health/live",
|
||||
() => Results.Ok(new GatewayHealthReply(
|
||||
Status: "Healthy",
|
||||
DefaultBackend: GatewayContractInfo.DefaultBackendName,
|
||||
WorkerProtocolVersion: GatewayContractInfo.WorkerProtocolVersion)))
|
||||
.WithName("LiveHealth");
|
||||
endpoints.MapZbHealth();
|
||||
|
||||
endpoints.MapGrpcService<MxAccessGatewayService>();
|
||||
endpoints.MapGrpcService<GalaxyRepositoryGrpcService>();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||
<PackageReference Include="ZB.MOM.WW.Health" Version="0.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.2" />
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
||||
|
||||
public sealed class AuthStoreHealthCheckTests
|
||||
{
|
||||
private static AuthSqliteConnectionFactory FactoryFor(string sqlitePath)
|
||||
{
|
||||
// GatewayOptions.Authentication and AuthenticationOptions.SqlitePath are both
|
||||
// init-only, so populate them through object initializers.
|
||||
var options = new GatewayOptions
|
||||
{
|
||||
Authentication = new AuthenticationOptions { SqlitePath = sqlitePath },
|
||||
};
|
||||
return new AuthSqliteConnectionFactory(Options.Create(options));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Healthy_WhenStoreReachable()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"authcheck-{Guid.NewGuid():N}.db");
|
||||
try
|
||||
{
|
||||
var check = new AuthStoreHealthCheck(FactoryFor(path));
|
||||
var result = await check.CheckHealthAsync(new HealthCheckContext());
|
||||
Assert.Equal(HealthStatus.Healthy, result.Status);
|
||||
}
|
||||
finally { if (File.Exists(path)) File.Delete(path); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unhealthy_WhenPathUnusable()
|
||||
{
|
||||
// A regular file used as a parent directory forces the open to fail.
|
||||
var bogus = Path.Combine(Path.GetTempPath(), $"authcheck-{Guid.NewGuid():N}");
|
||||
await File.WriteAllTextAsync(bogus, "x");
|
||||
try
|
||||
{
|
||||
var check = new AuthStoreHealthCheck(FactoryFor(Path.Combine(bogus, "store.db")));
|
||||
var result = await check.CheckHealthAsync(new HealthCheckContext());
|
||||
Assert.Equal(HealthStatus.Unhealthy, result.Status);
|
||||
}
|
||||
finally { if (File.Exists(bogus)) File.Delete(bogus); }
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,22 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||
|
||||
public sealed class GatewayApplicationTests
|
||||
{
|
||||
/// <summary>Verifies that Build maps the live health check endpoint.</summary>
|
||||
/// <summary>Verifies that Build maps the canonical three health tiers.</summary>
|
||||
[Fact]
|
||||
public async Task Build_MapsLiveHealthEndpoint()
|
||||
public async Task Build_MapsCanonicalHealthEndpoints()
|
||||
{
|
||||
await using WebApplication app = GatewayApplication.Build([]);
|
||||
|
||||
RouteEndpoint endpoint = Assert.Single(
|
||||
((IEndpointRouteBuilder)app).DataSources
|
||||
.SelectMany(dataSource => dataSource.Endpoints)
|
||||
.OfType<RouteEndpoint>(),
|
||||
candidate => candidate.RoutePattern.RawText == "/health/live");
|
||||
var paths = ((IEndpointRouteBuilder)app).DataSources
|
||||
.SelectMany(dataSource => dataSource.Endpoints)
|
||||
.OfType<RouteEndpoint>()
|
||||
.Select(e => e.RoutePattern.RawText)
|
||||
.ToHashSet();
|
||||
|
||||
Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName);
|
||||
Assert.Contains("/health/ready", paths);
|
||||
Assert.Contains("/health/active", paths);
|
||||
Assert.Contains("/healthz", paths);
|
||||
Assert.DoesNotContain("/health/live", paths);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Build registers the gateway metrics service.</summary>
|
||||
|
||||
Reference in New Issue
Block a user