using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using YarpGateway.Data; using YarpGateway.Config; using YarpGateway.Models; using YarpGateway.Services; namespace YarpGateway.Controllers; [ApiController] [Route("api/gateway")] public class GatewayConfigController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly DatabaseRouteConfigProvider _routeProvider; private readonly DatabaseClusterConfigProvider _clusterProvider; private readonly IRouteCache _routeCache; public GatewayConfigController( IDbContextFactory dbContextFactory, DatabaseRouteConfigProvider routeProvider, DatabaseClusterConfigProvider clusterProvider, IRouteCache routeCache) { _dbContextFactory = dbContextFactory; _routeProvider = routeProvider; _clusterProvider = clusterProvider; _routeCache = routeCache; } #region Tenants [HttpGet("tenants")] public async Task GetTenants([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? keyword = null) { await using var db = _dbContextFactory.CreateDbContext(); var query = db.Tenants.Where(t => !t.IsDeleted); if (!string.IsNullOrEmpty(keyword)) { query = query.Where(t => t.TenantCode.Contains(keyword) || t.TenantName.Contains(keyword)); } var total = await query.CountAsync(); var items = await query .OrderByDescending(t => t.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(t => new { t.Id, t.TenantCode, t.TenantName, t.Status, RouteCount = db.TenantRoutes.Count(r => r.TenantCode == t.TenantCode && !r.IsDeleted), t.Version, t.CreatedTime, t.UpdatedTime }) .ToListAsync(); return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) }); } [HttpGet("tenants/{id}")] public async Task GetTenant(long id) { await using var db = _dbContextFactory.CreateDbContext(); var tenant = await db.Tenants.FindAsync(id); if (tenant == null) return NotFound(); return Ok(tenant); } [HttpPost("tenants")] public async Task CreateTenant([FromBody] CreateTenantDto dto) { await using var db = _dbContextFactory.CreateDbContext(); var existing = await db.Tenants.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode); if (existing != null) return BadRequest($"Tenant code {dto.TenantCode} already exists"); var tenant = new GwTenant { Id = GenerateId(), TenantCode = dto.TenantCode, TenantName = dto.TenantName, Status = 1, Version = 1 }; await db.Tenants.AddAsync(tenant); await db.SaveChangesAsync(); return Ok(tenant); } [HttpPut("tenants/{id}")] public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { await using var db = _dbContextFactory.CreateDbContext(); var tenant = await db.Tenants.FindAsync(id); if (tenant == null) return NotFound(); if (!string.IsNullOrEmpty(dto.TenantName)) tenant.TenantName = dto.TenantName; if (dto.Status != null) tenant.Status = dto.Status.Value; tenant.Version++; tenant.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); return Ok(tenant); } [HttpDelete("tenants/{id}")] public async Task DeleteTenant(long id) { await using var db = _dbContextFactory.CreateDbContext(); var tenant = await db.Tenants.FindAsync(id); if (tenant == null) return NotFound(); tenant.IsDeleted = true; tenant.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); return Ok(); } #endregion #region Routes [HttpGet("routes")] public async Task GetRoutes([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? tenantCode = null, [FromQuery] bool? isGlobal = null) { await using var db = _dbContextFactory.CreateDbContext(); var query = db.TenantRoutes.Where(r => !r.IsDeleted); if (!string.IsNullOrEmpty(tenantCode)) query = query.Where(r => r.TenantCode == tenantCode); if (isGlobal != null) query = query.Where(r => r.IsGlobal == isGlobal.Value); var total = await query.CountAsync(); var items = await query .OrderBy(r => r.Priority) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) }); } [HttpGet("routes/global")] public async Task GetGlobalRoutes() { await using var db = _dbContextFactory.CreateDbContext(); var routes = await db.TenantRoutes.Where(r => r.IsGlobal && !r.IsDeleted).ToListAsync(); return Ok(routes); } [HttpGet("routes/tenant/{tenantCode}")] public async Task GetTenantRoutes(string tenantCode) { await using var db = _dbContextFactory.CreateDbContext(); var routes = await db.TenantRoutes.Where(r => r.TenantCode == tenantCode && !r.IsDeleted).ToListAsync(); return Ok(routes); } [HttpGet("routes/{id}")] public async Task GetRoute(long id) { await using var db = _dbContextFactory.CreateDbContext(); var route = await db.TenantRoutes.FindAsync(id); if (route == null) return NotFound(); return Ok(route); } [HttpPost("routes")] public async Task CreateRoute([FromBody] CreateRouteDto dto) { await using var db = _dbContextFactory.CreateDbContext(); if ((dto.IsGlobal != true) && !string.IsNullOrEmpty(dto.TenantCode)) { var tenant = await db.Tenants.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode); if (tenant == null) return BadRequest($"Tenant {dto.TenantCode} not found"); } var route = new GwTenantRoute { Id = GenerateId(), TenantCode = dto.TenantCode ?? string.Empty, ServiceName = dto.ServiceName, ClusterId = dto.ClusterId, PathPattern = dto.PathPattern, Priority = dto.Priority ?? 10, Status = 1, IsGlobal = dto.IsGlobal ?? false, Version = 1, CreatedTime = DateTime.UtcNow }; await db.TenantRoutes.AddAsync(route); await db.SaveChangesAsync(); await _routeCache.ReloadAsync(); return Ok(route); } [HttpPut("routes/{id}")] public async Task UpdateRoute(long id, [FromBody] CreateRouteDto dto) { await using var db = _dbContextFactory.CreateDbContext(); var route = await db.TenantRoutes.FindAsync(id); if (route == null) return NotFound(); route.ServiceName = dto.ServiceName; route.ClusterId = dto.ClusterId; route.PathPattern = dto.PathPattern; if (dto.Priority != null) route.Priority = dto.Priority.Value; route.Version++; route.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); await _routeCache.ReloadAsync(); return Ok(route); } [HttpDelete("routes/{id}")] public async Task DeleteRoute(long id) { await using var db = _dbContextFactory.CreateDbContext(); var route = await db.TenantRoutes.FindAsync(id); if (route == null) return NotFound(); route.IsDeleted = true; route.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); await _routeCache.ReloadAsync(); return Ok(); } #endregion #region Clusters [HttpGet("clusters")] public async Task GetClusters() { await using var db = _dbContextFactory.CreateDbContext(); var clusters = await db.ServiceInstances .Where(i => !i.IsDeleted) .GroupBy(i => i.ClusterId) .Select(g => new { ClusterId = g.Key, ClusterName = g.Key, InstanceCount = g.Count(), HealthyInstanceCount = g.Count(i => i.Health == 1), Instances = g.ToList() }) .ToListAsync(); return Ok(clusters); } [HttpGet("clusters/{clusterId}")] public async Task GetCluster(string clusterId) { await using var db = _dbContextFactory.CreateDbContext(); var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId && !i.IsDeleted).ToListAsync(); if (!instances.Any()) return NotFound(); return Ok(new { ClusterId = clusterId, ClusterName = clusterId, InstanceCount = instances.Count, HealthyInstanceCount = instances.Count(i => i.Health == 1), Instances = instances }); } [HttpPost("clusters")] public async Task CreateCluster([FromBody] CreateClusterDto dto) { return Ok(new { message = "Cluster created", clusterId = dto.ClusterId }); } [HttpDelete("clusters/{clusterId}")] public async Task DeleteCluster(string clusterId) { await using var db = _dbContextFactory.CreateDbContext(); var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId).ToListAsync(); foreach (var instance in instances) { instance.IsDeleted = true; } await db.SaveChangesAsync(); await _clusterProvider.ReloadAsync(); return Ok(); } #endregion #region Instances [HttpGet("clusters/{clusterId}/instances")] public async Task GetInstances(string clusterId) { await using var db = _dbContextFactory.CreateDbContext(); var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId && !i.IsDeleted).ToListAsync(); return Ok(instances); } [HttpGet("instances/{id}")] public async Task GetInstance(long id) { await using var db = _dbContextFactory.CreateDbContext(); var instance = await db.ServiceInstances.FindAsync(id); if (instance == null) return NotFound(); return Ok(instance); } [HttpPost("clusters/{clusterId}/instances")] public async Task CreateInstance(string clusterId, [FromBody] CreateInstanceDto dto) { await using var db = _dbContextFactory.CreateDbContext(); var existing = await db.ServiceInstances.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == dto.DestinationId); if (existing != null) return BadRequest($"Instance {dto.DestinationId} already exists"); var instance = new GwServiceInstance { Id = GenerateId(), ClusterId = clusterId, DestinationId = dto.DestinationId, Address = dto.Address, Weight = dto.Weight ?? 1, Health = dto.IsHealthy == true ? 1 : 0, Status = 1, Version = 1, CreatedTime = DateTime.UtcNow }; await db.ServiceInstances.AddAsync(instance); await db.SaveChangesAsync(); await _clusterProvider.ReloadAsync(); return Ok(instance); } [HttpDelete("instances/{id}")] public async Task DeleteInstance(long id) { await using var db = _dbContextFactory.CreateDbContext(); var instance = await db.ServiceInstances.FindAsync(id); if (instance == null) return NotFound(); instance.IsDeleted = true; instance.UpdatedTime = DateTime.UtcNow; await db.SaveChangesAsync(); await _clusterProvider.ReloadAsync(); return Ok(); } #endregion #region Config & Stats [HttpPost("config/reload")] public async Task ReloadConfig() { await _routeCache.ReloadAsync(); await _routeProvider.ReloadAsync(); await _clusterProvider.ReloadAsync(); return Ok(new { message = "Config reloaded successfully", timestamp = DateTime.UtcNow }); } [HttpGet("config/status")] public async Task GetConfigStatus() { await using var db = _dbContextFactory.CreateDbContext(); var routeCount = await db.TenantRoutes.CountAsync(r => r.Status == 1 && !r.IsDeleted); var instanceCount = await db.ServiceInstances.CountAsync(i => i.Status == 1 && !i.IsDeleted); var healthyCount = await db.ServiceInstances.CountAsync(i => i.Health == 1 && !i.IsDeleted); return Ok(new { routeCount, clusterCount = await db.ServiceInstances.Where(i => !i.IsDeleted).GroupBy(i => i.ClusterId).CountAsync(), instanceCount, healthyInstanceCount = healthyCount, lastReloadTime = DateTime.UtcNow, isListening = true, listenerStatus = "Active" }); } [HttpGet("config/versions")] public async Task GetVersionInfo() { await using var db = _dbContextFactory.CreateDbContext(); var routeVersion = await db.TenantRoutes.OrderByDescending(r => r.Version).Select(r => r.Version).FirstOrDefaultAsync(); var clusterVersion = await db.ServiceInstances.OrderByDescending(i => i.Version).Select(i => i.Version).FirstOrDefaultAsync(); return Ok(new { routeVersion, clusterVersion, routeVersionUpdatedAt = DateTime.UtcNow, clusterVersionUpdatedAt = DateTime.UtcNow }); } [HttpGet("stats/overview")] public async Task GetOverviewStats() { await using var db = _dbContextFactory.CreateDbContext(); var totalTenants = await db.Tenants.CountAsync(t => !t.IsDeleted); var activeTenants = await db.Tenants.CountAsync(t => !t.IsDeleted && t.Status == 1); var totalRoutes = await db.TenantRoutes.CountAsync(r => r.Status == 1 && !r.IsDeleted); var totalInstances = await db.ServiceInstances.CountAsync(i => i.Status == 1 && !i.IsDeleted); var healthyInstances = await db.ServiceInstances.CountAsync(i => i.Health == 1 && !i.IsDeleted); return Ok(new { totalTenants, activeTenants, totalRoutes, totalClusters = await db.ServiceInstances.Where(i => !i.IsDeleted).GroupBy(i => i.ClusterId).CountAsync(), totalInstances, healthyInstances, lastUpdated = DateTime.UtcNow }); } #endregion #region DTOs public class CreateTenantDto { public string TenantCode { get; set; } = string.Empty; public string TenantName { get; set; } = string.Empty; } public class UpdateTenantDto { public string? TenantName { get; set; } public int? Status { get; set; } } public class CreateRouteDto { public string? TenantCode { get; set; } public string ServiceName { get; set; } = string.Empty; public string ClusterId { get; set; } = string.Empty; public string PathPattern { get; set; } = string.Empty; public int? Priority { get; set; } public bool? IsGlobal { get; set; } } public class CreateClusterDto { public string ClusterId { get; set; } = string.Empty; public string ClusterName { get; set; } = string.Empty; public string? Description { get; set; } public string? LoadBalancingPolicy { get; set; } } public class CreateInstanceDto { public string DestinationId { get; set; } = string.Empty; public string Address { get; set; } = string.Empty; public int? Weight { get; set; } public bool? IsHealthy { get; set; } } #endregion private long GenerateId() { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } }