skoruba / Duende.IdentityServer.Admin

The administration for the Duende IdentityServer and Asp.Net Core Identity ⚡
Apache License 2.0
549 stars 194 forks source link

Admin UI healthchecks fail if you have namespaces for your *DbContexts #135

Open toby-freemarket opened 1 year ago

toby-freemarket commented 1 year ago

Describe the bug

This is an issue with Identity Server Admin UI Hi I've found and issue if you add namespaces to your database contexts, the code that adds the healthchecks is here https://github.com/skoruba/Duende.IdentityServer.Admin/blob/a07a55f2bf12f53e385eff861328637d90a7d1f6/src/Skoruba.Duende.IdentityServer.Admin.UI/Helpers/DependencyInjection/AdminUIServiceCollectionExtensions.cs#L190-L193

There is a factory but it only allows me to add extra ones not replace the current ones. When I do it crashes on start up saying I'm trying to add duplicates. This is causes me an issue as it's always unhealthly in azure app services.

{
    "status": "Unhealthy",
    "totalDuration": "00:00:00.7737005",
    "entries": {
        "ConfigurationDbContext": {
            "data": {},
            "duration": "00:00:00.1877310",
            "status": "Healthy",
            "tags": []
        },
        "PersistedGrantsDbContext": {
            "data": {},
            "duration": "00:00:00.1877500",
            "status": "Healthy",
            "tags": []
        },
        "IdentityDbContext": {
            "data": {},
            "duration": "00:00:00.1840748",
            "status": "Healthy",
            "tags": []
        },
        "LogDbContext": {
            "data": {},
            "duration": "00:00:00.1876931",
            "status": "Healthy",
            "tags": []
        },
        "AuditLogDbContext": {
            "data": {},
            "duration": "00:00:00.1877340",
            "status": "Healthy",
            "tags": []
        },
        "DataProtectionDbContext": {
            "data": {},
            "duration": "00:00:00.1806620",
            "status": "Healthy",
            "tags": []
        },
        "Identity Server": {
            "data": {},
            "duration": "00:00:00.7337041",
            "status": "Healthy",
            "tags": []
        },
        "ConfigurationDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.ApiResources\u0027.",
            "duration": "00:00:00.1009983",
            "exception": "Invalid object name \u0027dbo.ApiResources\u0027.",
            "status": "Unhealthy",
            "tags": []
        },
        "PersistentGrantsDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.DeviceCodes\u0027.",
            "duration": "00:00:00.0948288",
            "exception": "Invalid object name \u0027dbo.DeviceCodes\u0027.",
            "status": "Unhealthy",
            "tags": []
        },
        "IdentityDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.Users\u0027.",
            "duration": "00:00:00.0947852",
            "exception": "Invalid object name \u0027dbo.Users\u0027.",
            "status": "Unhealthy",
            "tags": []
        },
        "LogDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.Log\u0027.",
            "duration": "00:00:00.0947824",
            "exception": "Invalid object name \u0027dbo.Log\u0027.",
            "status": "Unhealthy",
            "tags": []
        },
        "AuditLogDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.AuditLog\u0027.",
            "duration": "00:00:00.1010349",
            "exception": "Invalid object name \u0027dbo.AuditLog\u0027.",
            "status": "Unhealthy",
            "tags": []
        },
        "DataProtectionDb": {
            "data": {},
            "description": "Invalid object name \u0027dbo.DataProtectionKeys\u0027.",
            "duration": "00:00:00.0947751",
            "exception": "Invalid object name \u0027dbo.DataProtectionKeys\u0027.",
            "status": "Unhealthy",
            "tags": []
        }
    }
}

Here is the healthchecks I would like to do.

        public static void AddIdSHealthChecks<TConfigurationDbContext, TPersistedGrantDbContext, TIdentityDbContext, TLogDbContext, TAuditLoggingDbContext, TDataProtectionDbContext>(this IServiceCollection services, IConfiguration configuration, Uri identityServerUri)
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TIdentityDbContext : DbContext
            where TLogDbContext : DbContext, IAdminLogDbContext
            where TAuditLoggingDbContext : DbContext, IAuditLoggingDbContext<AuditLog>
            where TDataProtectionDbContext : DbContext, IDataProtectionKeyContext
        {
            var configurationDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey);
            var persistedGrantsDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey);
            var identityDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
            var logDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.AdminLogDbConnectionStringKey);
            var auditLogDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.AdminAuditLogDbConnectionStringKey);
            var dataProtectionDbConnectionString = configuration.GetConnectionString(ConfigurationConsts.DataProtectionDbConnectionStringKey);

            var healthChecksBuilder = services.AddHealthChecks()
                .AddDbContextCheck<TConfigurationDbContext>("ConfigurationDbContext")
                .AddDbContextCheck<TPersistedGrantDbContext>("PersistedGrantsDbContext")
                .AddDbContextCheck<TIdentityDbContext>("IdentityDbContext")
                .AddDbContextCheck<TLogDbContext>("LogDbContext")
                .AddDbContextCheck<TAuditLoggingDbContext>("AuditLogDbContext")
                .AddDbContextCheck<TDataProtectionDbContext>("DataProtectionDbContext")
                .AddIdentityServer(identityServerUri, "Identity Server");

            var serviceProvider = services.BuildServiceProvider();
            var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var configurationTableName = DbContextHelpers.GetEntityTable<TConfigurationDbContext>(scope.ServiceProvider);
                var persistedGrantTableName = DbContextHelpers.GetEntityTable<TPersistedGrantDbContext>(scope.ServiceProvider);
                var identityTableName = DbContextHelpers.GetEntityTable<TIdentityDbContext>(scope.ServiceProvider);
                var logTableName = DbContextHelpers.GetEntityTable<TLogDbContext>(scope.ServiceProvider);
                var auditLogTableName = DbContextHelpers.GetEntityTable<TAuditLoggingDbContext>(scope.ServiceProvider);
                var dataProtectionTableName = DbContextHelpers.GetEntityTable<TDataProtectionDbContext>(scope.ServiceProvider);

                healthChecksBuilder
                    .AddSqlServer(configurationDbConnectionString, name: "ConfigurationDb",
                        healthQuery: $"SELECT TOP 1 * FROM [Configuration].[{configurationTableName}]")
                    .AddSqlServer(persistedGrantsDbConnectionString, name: "PersistentGrantsDb",
                        healthQuery: $"SELECT TOP 1 * FROM [Operational].[{persistedGrantTableName}]")
                    .AddSqlServer(identityDbConnectionString, name: "IdentityDb",
                        healthQuery: $"SELECT TOP 1 * FROM [Identity].[{identityTableName}]")
                    .AddSqlServer(logDbConnectionString, name: "LogDb",
                        healthQuery: $"SELECT TOP 1 * FROM [Identity].[{logTableName}]")
                    .AddSqlServer(auditLogDbConnectionString, name: "AuditLogDb",
                        healthQuery: $"SELECT TOP 1 * FROM [Identity].[{auditLogTableName}]")
                    .AddSqlServer(dataProtectionDbConnectionString, name: "DataProtectionDb",
                    healthQuery: $"SELECT TOP 1 * FROM [DataProtection].[{dataProtectionTableName}]");
            }
        }
toby-freemarket commented 1 year ago

Looking a little deeper into healthchecks services.AddHealthChecks().AddDbContextCheck<DbContext>() does a .CanConnectAsync() to test if the database is there. Instead of 2 health checks you could do the checks this way using AddDbContextCheck with customTestQuery

.AddDbContextCheck<TConfigurationDbContext>("ConfigurationDbContext", customTestQuery: (context, token) => context.ApiResources.AnyAsync(token))
skoruba commented 1 year ago

Nice, can you send a PR with this one? thank you

toby-freemarket commented 1 year ago

Sure should be able to do it tomorrow

apetrut commented 1 year ago

Hi @toby-freemarket ,

I've got also health check issues for Identity Server. We are running inside K8S on a dev environment.

image

This is all I've got in the logs (see above).

Have you got this type of issues before?

Thanks.