fengling-gateway/Data/GatewayDbContext.cs
movingsam abe3456ccb feat[gateway]: add K8s service discovery with pending approval workflow
- Add PendingServiceDiscovery model and database migration
- Add PendingServices API controller for service assignment
- Add KubernetesPendingSyncService for background sync
- Add RBAC configuration for K8s service discovery
- Update Dockerfile and K8s deployment configs
- Add service discovery design documentation

Workflow: K8s services with label managed-by=yarp are discovered
and stored in pending table. Admin approves before they become
active gateway downstream services.
2026-02-22 22:14:54 +08:00

142 lines
5.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using Npgsql;
using YarpGateway.Config;
using YarpGateway.Models;
namespace YarpGateway.Data;
public class GatewayDbContext : DbContext
{
public GatewayDbContext(DbContextOptions<GatewayDbContext> options)
: base(options)
{
}
public DbSet<GwTenant> Tenants => Set<GwTenant>();
public DbSet<GwTenantRoute> TenantRoutes => Set<GwTenantRoute>();
public DbSet<GwServiceInstance> ServiceInstances => Set<GwServiceInstance>();
public DbSet<GwPendingServiceDiscovery> PendingServiceDiscoveries => Set<GwPendingServiceDiscovery>();
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
DetectConfigChanges();
var result = base.SaveChanges(acceptAllChangesOnSuccess);
if (_configChangeDetected)
{
NotifyConfigChangedSync();
}
return result;
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
DetectConfigChanges();
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
if (_configChangeDetected)
{
await NotifyConfigChangedAsync(cancellationToken);
}
return result;
}
private bool _configChangeDetected;
private void DetectConfigChanges()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
.Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant);
_configChangeDetected = entries.Any();
}
private bool IsRelationalDatabase()
{
try
{
return Database.IsRelational();
}
catch
{
return false;
}
}
private void NotifyConfigChangedSync()
{
if (!IsRelationalDatabase()) return;
var connectionString = Database.GetConnectionString();
if (string.IsNullOrEmpty(connectionString)) return;
using var connection = new NpgsqlConnection(connectionString);
connection.Open();
using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection);
cmd.ExecuteNonQuery();
}
private async Task NotifyConfigChangedAsync(CancellationToken cancellationToken)
{
if (!IsRelationalDatabase()) return;
var connectionString = Database.GetConnectionString();
if (string.IsNullOrEmpty(connectionString)) return;
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection);
await cmd.ExecuteNonQueryAsync(cancellationToken);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GwTenant>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.TenantCode).HasMaxLength(50).IsRequired();
entity.Property(e => e.TenantName).HasMaxLength(100).IsRequired();
entity.HasIndex(e => e.TenantCode).IsUnique();
});
modelBuilder.Entity<GwTenantRoute>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.TenantCode).HasMaxLength(50);
entity.Property(e => e.ServiceName).HasMaxLength(100).IsRequired();
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
entity.Property(e => e.PathPattern).HasMaxLength(200).IsRequired();
entity.HasIndex(e => e.TenantCode);
entity.HasIndex(e => e.ServiceName);
entity.HasIndex(e => e.ClusterId);
entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status });
});
modelBuilder.Entity<GwServiceInstance>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
entity.Property(e => e.DestinationId).HasMaxLength(100).IsRequired();
entity.Property(e => e.Address).HasMaxLength(200).IsRequired();
entity.HasIndex(e => new { e.ClusterId, e.DestinationId }).IsUnique();
entity.HasIndex(e => e.Health);
});
modelBuilder.Entity<GwPendingServiceDiscovery>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.K8sServiceName).HasMaxLength(255).IsRequired();
entity.Property(e => e.K8sNamespace).HasMaxLength(255).IsRequired();
entity.Property(e => e.K8sClusterIP).HasMaxLength(50);
entity.Property(e => e.DiscoveredPorts).HasMaxLength(500);
entity.Property(e => e.Labels).HasMaxLength(2000);
entity.Property(e => e.AssignedClusterId).HasMaxLength(100);
entity.Property(e => e.AssignedBy).HasMaxLength(100);
entity.HasIndex(e => new { e.K8sServiceName, e.K8sNamespace, e.IsDeleted }).IsUnique();
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.DiscoveredAt);
});
base.OnModelCreating(modelBuilder);
}
}