- 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.
142 lines
5.2 KiB
C#
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);
|
|
}
|
|
}
|