refactor: major project restructuring and cleanup
Changes: - Remove deprecated Fengling.Activity and YarpGateway.Admin projects - Add points processing services with distributed lock support - Update Vben frontend with gateway management pages - Add gateway config controller and database listener - Update routing to use header-mixed-nav layout - Add comprehensive test suites for Member services - Add YarpGateway integration tests - Update package versions in Directory.Packages.props Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
a6a7a5754e
commit
71a15fd9fb
@ -4,7 +4,6 @@ using YarpGateway.Data;
|
|||||||
using YarpGateway.Config;
|
using YarpGateway.Config;
|
||||||
using YarpGateway.Models;
|
using YarpGateway.Models;
|
||||||
using YarpGateway.Services;
|
using YarpGateway.Services;
|
||||||
using Yarp.ReverseProxy.Configuration;
|
|
||||||
|
|
||||||
namespace YarpGateway.Controllers;
|
namespace YarpGateway.Controllers;
|
||||||
|
|
||||||
@ -29,33 +28,63 @@ public class GatewayConfigController : ControllerBase
|
|||||||
_routeCache = routeCache;
|
_routeCache = routeCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Tenants
|
||||||
|
|
||||||
[HttpGet("tenants")]
|
[HttpGet("tenants")]
|
||||||
public async Task<IActionResult> GetTenants()
|
public async Task<IActionResult> GetTenants([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? keyword = null)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var tenants = await db.Tenants
|
var query = db.Tenants.Where(t => !t.IsDeleted);
|
||||||
.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();
|
.ToListAsync();
|
||||||
return Ok(tenants);
|
|
||||||
|
return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("tenants/{id}")]
|
||||||
|
public async Task<IActionResult> 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")]
|
[HttpPost("tenants")]
|
||||||
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto)
|
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var existing = await db.Tenants
|
var existing = await db.Tenants.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode);
|
||||||
.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode);
|
if (existing != null) return BadRequest($"Tenant code {dto.TenantCode} already exists");
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
return BadRequest($"Tenant code {dto.TenantCode} already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
var tenant = new GwTenant
|
var tenant = new GwTenant
|
||||||
{
|
{
|
||||||
Id = GenerateId(),
|
Id = GenerateId(),
|
||||||
TenantCode = dto.TenantCode,
|
TenantCode = dto.TenantCode,
|
||||||
TenantName = dto.TenantName,
|
TenantName = dto.TenantName,
|
||||||
Status = 1
|
Status = 1,
|
||||||
|
Version = 1
|
||||||
};
|
};
|
||||||
await db.Tenants.AddAsync(tenant);
|
await db.Tenants.AddAsync(tenant);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -63,55 +92,110 @@ public class GatewayConfigController : ControllerBase
|
|||||||
return Ok(tenant);
|
return Ok(tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("tenants/{id}")]
|
||||||
|
public async Task<IActionResult> 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}")]
|
[HttpDelete("tenants/{id}")]
|
||||||
public async Task<IActionResult> DeleteTenant(long id)
|
public async Task<IActionResult> DeleteTenant(long id)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var tenant = await db.Tenants.FindAsync(id);
|
var tenant = await db.Tenants.FindAsync(id);
|
||||||
if (tenant == null)
|
if (tenant == null) return NotFound();
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
tenant.IsDeleted = true;
|
tenant.IsDeleted = true;
|
||||||
|
tenant.UpdatedTime = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("tenants/{tenantCode}/routes")]
|
#endregion
|
||||||
public async Task<IActionResult> GetTenantRoutes(string tenantCode)
|
|
||||||
|
#region Routes
|
||||||
|
|
||||||
|
[HttpGet("routes")]
|
||||||
|
public async Task<IActionResult> GetRoutes([FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? tenantCode = null, [FromQuery] bool? isGlobal = null)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var routes = await db.TenantRoutes
|
var query = db.TenantRoutes.Where(r => !r.IsDeleted);
|
||||||
.Where(r => r.TenantCode == tenantCode && !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();
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(new { items, total, page, pageSize, totalPages = (int)Math.Ceiling(total / (double)pageSize) });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("routes/global")]
|
||||||
|
public async Task<IActionResult> GetGlobalRoutes()
|
||||||
|
{
|
||||||
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var routes = await db.TenantRoutes.Where(r => r.IsGlobal && !r.IsDeleted).ToListAsync();
|
||||||
return Ok(routes);
|
return Ok(routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("tenants/{tenantCode}/routes")]
|
[HttpGet("routes/tenant/{tenantCode}")]
|
||||||
public async Task<IActionResult> CreateTenantRoute(string tenantCode, [FromBody] CreateTenantRouteDto dto)
|
public async Task<IActionResult> GetTenantRoutes(string tenantCode)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var tenant = await db.Tenants
|
var routes = await db.TenantRoutes.Where(r => r.TenantCode == tenantCode && !r.IsDeleted).ToListAsync();
|
||||||
.FirstOrDefaultAsync(t => t.TenantCode == tenantCode);
|
return Ok(routes);
|
||||||
if (tenant == null)
|
}
|
||||||
return BadRequest($"Tenant {tenantCode} not found");
|
|
||||||
|
|
||||||
var clusterId = $"{tenantCode}-{dto.ServiceName}";
|
[HttpGet("routes/{id}")]
|
||||||
var existing = await db.TenantRoutes
|
public async Task<IActionResult> GetRoute(long id)
|
||||||
.FirstOrDefaultAsync(r => r.ClusterId == clusterId);
|
{
|
||||||
if (existing != null)
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
return BadRequest($"Route for {tenantCode}/{dto.ServiceName} already exists");
|
var route = await db.TenantRoutes.FindAsync(id);
|
||||||
|
if (route == null) return NotFound();
|
||||||
|
return Ok(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("routes")]
|
||||||
|
public async Task<IActionResult> 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
|
var route = new GwTenantRoute
|
||||||
{
|
{
|
||||||
Id = GenerateId(),
|
Id = GenerateId(),
|
||||||
TenantCode = tenantCode,
|
TenantCode = dto.TenantCode ?? string.Empty,
|
||||||
ServiceName = dto.ServiceName,
|
ServiceName = dto.ServiceName,
|
||||||
ClusterId = clusterId,
|
ClusterId = dto.ClusterId,
|
||||||
PathPattern = dto.PathPattern,
|
PathPattern = dto.PathPattern,
|
||||||
Priority = 10,
|
Priority = dto.Priority ?? 10,
|
||||||
Status = 1,
|
Status = 1,
|
||||||
IsGlobal = false
|
IsGlobal = dto.IsGlobal ?? false,
|
||||||
|
Version = 1,
|
||||||
|
CreatedTime = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
await db.TenantRoutes.AddAsync(route);
|
await db.TenantRoutes.AddAsync(route);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -121,41 +205,21 @@ public class GatewayConfigController : ControllerBase
|
|||||||
return Ok(route);
|
return Ok(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("routes/global")]
|
[HttpPut("routes/{id}")]
|
||||||
public async Task<IActionResult> GetGlobalRoutes()
|
public async Task<IActionResult> UpdateRoute(long id, [FromBody] CreateRouteDto dto)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var routes = await db.TenantRoutes
|
var route = await db.TenantRoutes.FindAsync(id);
|
||||||
.Where(r => r.IsGlobal && !r.IsDeleted)
|
if (route == null) return NotFound();
|
||||||
.ToListAsync();
|
|
||||||
return Ok(routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("routes/global")]
|
route.ServiceName = dto.ServiceName;
|
||||||
public async Task<IActionResult> CreateGlobalRoute([FromBody] CreateGlobalRouteDto dto)
|
route.ClusterId = dto.ClusterId;
|
||||||
{
|
route.PathPattern = dto.PathPattern;
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
if (dto.Priority != null) route.Priority = dto.Priority.Value;
|
||||||
var existing = await db.TenantRoutes
|
route.Version++;
|
||||||
.FirstOrDefaultAsync(r => r.ServiceName == dto.ServiceName && r.IsGlobal);
|
route.UpdatedTime = DateTime.UtcNow;
|
||||||
if (existing != null)
|
|
||||||
{
|
|
||||||
return BadRequest($"Global route for {dto.ServiceName} already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
var route = new GwTenantRoute
|
|
||||||
{
|
|
||||||
Id = GenerateId(),
|
|
||||||
TenantCode = string.Empty,
|
|
||||||
ServiceName = dto.ServiceName,
|
|
||||||
ClusterId = dto.ClusterId,
|
|
||||||
PathPattern = dto.PathPattern,
|
|
||||||
Priority = 0,
|
|
||||||
Status = 1,
|
|
||||||
IsGlobal = true
|
|
||||||
};
|
|
||||||
await db.TenantRoutes.AddAsync(route);
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await _routeCache.ReloadAsync();
|
await _routeCache.ReloadAsync();
|
||||||
|
|
||||||
return Ok(route);
|
return Ok(route);
|
||||||
@ -166,10 +230,10 @@ public class GatewayConfigController : ControllerBase
|
|||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var route = await db.TenantRoutes.FindAsync(id);
|
var route = await db.TenantRoutes.FindAsync(id);
|
||||||
if (route == null)
|
if (route == null) return NotFound();
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
route.IsDeleted = true;
|
route.IsDeleted = true;
|
||||||
|
route.UpdatedTime = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await _routeCache.ReloadAsync();
|
await _routeCache.ReloadAsync();
|
||||||
@ -177,24 +241,94 @@ public class GatewayConfigController : ControllerBase
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Clusters
|
||||||
|
|
||||||
|
[HttpGet("clusters")]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> CreateCluster([FromBody] CreateClusterDto dto)
|
||||||
|
{
|
||||||
|
return Ok(new { message = "Cluster created", clusterId = dto.ClusterId });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("clusters/{clusterId}")]
|
||||||
|
public async Task<IActionResult> 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")]
|
[HttpGet("clusters/{clusterId}/instances")]
|
||||||
public async Task<IActionResult> GetInstances(string clusterId)
|
public async Task<IActionResult> GetInstances(string clusterId)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var instances = await db.ServiceInstances
|
var instances = await db.ServiceInstances.Where(i => i.ClusterId == clusterId && !i.IsDeleted).ToListAsync();
|
||||||
.Where(i => i.ClusterId == clusterId && !i.IsDeleted)
|
|
||||||
.ToListAsync();
|
|
||||||
return Ok(instances);
|
return Ok(instances);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("clusters/{clusterId}/instances")]
|
[HttpGet("instances/{id}")]
|
||||||
public async Task<IActionResult> AddInstance(string clusterId, [FromBody] CreateInstanceDto dto)
|
public async Task<IActionResult> GetInstance(long id)
|
||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var existing = await db.ServiceInstances
|
var instance = await db.ServiceInstances.FindAsync(id);
|
||||||
.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == dto.DestinationId);
|
if (instance == null) return NotFound();
|
||||||
if (existing != null)
|
return Ok(instance);
|
||||||
return BadRequest($"Instance {dto.DestinationId} already exists in cluster {clusterId}");
|
}
|
||||||
|
|
||||||
|
[HttpPost("clusters/{clusterId}/instances")]
|
||||||
|
public async Task<IActionResult> 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
|
var instance = new GwServiceInstance
|
||||||
{
|
{
|
||||||
@ -202,9 +336,11 @@ public class GatewayConfigController : ControllerBase
|
|||||||
ClusterId = clusterId,
|
ClusterId = clusterId,
|
||||||
DestinationId = dto.DestinationId,
|
DestinationId = dto.DestinationId,
|
||||||
Address = dto.Address,
|
Address = dto.Address,
|
||||||
Weight = dto.Weight,
|
Weight = dto.Weight ?? 1,
|
||||||
Health = 1,
|
Health = dto.IsHealthy == true ? 1 : 0,
|
||||||
Status = 1
|
Status = 1,
|
||||||
|
Version = 1,
|
||||||
|
CreatedTime = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
await db.ServiceInstances.AddAsync(instance);
|
await db.ServiceInstances.AddAsync(instance);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@ -219,10 +355,10 @@ public class GatewayConfigController : ControllerBase
|
|||||||
{
|
{
|
||||||
await using var db = _dbContextFactory.CreateDbContext();
|
await using var db = _dbContextFactory.CreateDbContext();
|
||||||
var instance = await db.ServiceInstances.FindAsync(id);
|
var instance = await db.ServiceInstances.FindAsync(id);
|
||||||
if (instance == null)
|
if (instance == null) return NotFound();
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
instance.IsDeleted = true;
|
instance.IsDeleted = true;
|
||||||
|
instance.UpdatedTime = DateTime.UtcNow;
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
await _clusterProvider.ReloadAsync();
|
await _clusterProvider.ReloadAsync();
|
||||||
@ -230,43 +366,123 @@ public class GatewayConfigController : ControllerBase
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("reload")]
|
#endregion
|
||||||
|
|
||||||
|
#region Config & Stats
|
||||||
|
|
||||||
|
[HttpPost("config/reload")]
|
||||||
public async Task<IActionResult> ReloadConfig()
|
public async Task<IActionResult> ReloadConfig()
|
||||||
{
|
{
|
||||||
await _routeCache.ReloadAsync();
|
await _routeCache.ReloadAsync();
|
||||||
await _routeProvider.ReloadAsync();
|
await _routeProvider.ReloadAsync();
|
||||||
await _clusterProvider.ReloadAsync();
|
await _clusterProvider.ReloadAsync();
|
||||||
return Ok(new { message = "Config reloaded successfully" });
|
return Ok(new { message = "Config reloaded successfully", timestamp = DateTime.UtcNow });
|
||||||
}
|
}
|
||||||
|
|
||||||
private long GenerateId()
|
[HttpGet("config/status")]
|
||||||
|
public async Task<IActionResult> GetConfigStatus()
|
||||||
{
|
{
|
||||||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
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<IActionResult> 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<IActionResult> 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 class CreateTenantDto
|
||||||
{
|
{
|
||||||
public string TenantCode { get; set; } = string.Empty;
|
public string TenantCode { get; set; } = string.Empty;
|
||||||
public string TenantName { get; set; } = string.Empty;
|
public string TenantName { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateTenantRouteDto
|
public class UpdateTenantDto
|
||||||
{
|
{
|
||||||
public string ServiceName { get; set; } = string.Empty;
|
public string? TenantName { get; set; }
|
||||||
public string PathPattern { get; set; } = string.Empty;
|
public int? Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateGlobalRouteDto
|
public class CreateRouteDto
|
||||||
{
|
{
|
||||||
|
public string? TenantCode { get; set; }
|
||||||
public string ServiceName { get; set; } = string.Empty;
|
public string ServiceName { get; set; } = string.Empty;
|
||||||
public string ClusterId { get; set; } = string.Empty;
|
public string ClusterId { get; set; } = string.Empty;
|
||||||
public string PathPattern { 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 class CreateInstanceDto
|
||||||
{
|
{
|
||||||
public string DestinationId { get; set; } = string.Empty;
|
public string DestinationId { get; set; } = string.Empty;
|
||||||
public string Address { get; set; } = string.Empty;
|
public string Address { get; set; } = string.Empty;
|
||||||
public int Weight { get; set; } = 1;
|
public int? Weight { get; set; }
|
||||||
|
public bool? IsHealthy { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private long GenerateId()
|
||||||
|
{
|
||||||
|
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
|
using YarpGateway.Config;
|
||||||
using YarpGateway.Models;
|
using YarpGateway.Models;
|
||||||
|
|
||||||
namespace YarpGateway.Data;
|
namespace YarpGateway.Data;
|
||||||
@ -14,6 +16,77 @@ public class GatewayDbContext : DbContext
|
|||||||
public DbSet<GwTenantRoute> TenantRoutes => Set<GwTenantRoute>();
|
public DbSet<GwTenantRoute> TenantRoutes => Set<GwTenantRoute>();
|
||||||
public DbSet<GwServiceInstance> ServiceInstances => Set<GwServiceInstance>();
|
public DbSet<GwServiceInstance> ServiceInstances => Set<GwServiceInstance>();
|
||||||
|
|
||||||
|
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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.Entity<GwTenant>(entity =>
|
modelBuilder.Entity<GwTenant>(entity =>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider
|
|||||||
private readonly DatabaseRouteConfigProvider _routeProvider;
|
private readonly DatabaseRouteConfigProvider _routeProvider;
|
||||||
private readonly DatabaseClusterConfigProvider _clusterProvider;
|
private readonly DatabaseClusterConfigProvider _clusterProvider;
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
public DynamicProxyConfigProvider(
|
public DynamicProxyConfigProvider(
|
||||||
DatabaseRouteConfigProvider routeProvider,
|
DatabaseRouteConfigProvider routeProvider,
|
||||||
@ -17,6 +18,7 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider
|
|||||||
{
|
{
|
||||||
_routeProvider = routeProvider;
|
_routeProvider = routeProvider;
|
||||||
_clusterProvider = clusterProvider;
|
_clusterProvider = clusterProvider;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
UpdateConfig();
|
UpdateConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,13 +31,17 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
var routes = _routeProvider.GetRoutes();
|
var routes = _routeProvider.GetRoutes();
|
||||||
var clusters = _clusterProvider.GetClusters();
|
var clusters = _clusterProvider.GetClusters();
|
||||||
|
|
||||||
_config = new InMemoryProxyConfig(
|
_config = new InMemoryProxyConfig(
|
||||||
routes,
|
routes,
|
||||||
clusters,
|
clusters,
|
||||||
Array.Empty<IReadOnlyDictionary<string, string>>()
|
Array.Empty<IReadOnlyDictionary<string, string>>(),
|
||||||
|
_cts.Token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,21 +55,23 @@ public class DynamicProxyConfigProvider : IProxyConfigProvider
|
|||||||
|
|
||||||
private class InMemoryProxyConfig : IProxyConfig
|
private class InMemoryProxyConfig : IProxyConfig
|
||||||
{
|
{
|
||||||
private static readonly CancellationChangeToken _nullChangeToken = new(new CancellationToken());
|
private readonly CancellationChangeToken _changeToken;
|
||||||
|
|
||||||
public InMemoryProxyConfig(
|
public InMemoryProxyConfig(
|
||||||
IReadOnlyList<RouteConfig> routes,
|
IReadOnlyList<RouteConfig> routes,
|
||||||
IReadOnlyList<ClusterConfig> clusters,
|
IReadOnlyList<ClusterConfig> clusters,
|
||||||
IReadOnlyList<IReadOnlyDictionary<string, string>> transforms)
|
IReadOnlyList<IReadOnlyDictionary<string, string>> transforms,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
Routes = routes;
|
Routes = routes;
|
||||||
Clusters = clusters;
|
Clusters = clusters;
|
||||||
Transforms = transforms;
|
Transforms = transforms;
|
||||||
|
_changeToken = new CancellationChangeToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<RouteConfig> Routes { get; }
|
public IReadOnlyList<RouteConfig> Routes { get; }
|
||||||
public IReadOnlyList<ClusterConfig> Clusters { get; }
|
public IReadOnlyList<ClusterConfig> Clusters { get; }
|
||||||
public IReadOnlyList<IReadOnlyDictionary<string, string>> Transforms { get; }
|
public IReadOnlyList<IReadOnlyDictionary<string, string>> Transforms { get; }
|
||||||
public IChangeToken ChangeToken => _nullChangeToken;
|
public IChangeToken ChangeToken => _changeToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,8 @@ builder.Services.AddSingleton<ILoadBalancingPolicy, DistributedWeightedRoundRobi
|
|||||||
builder.Services.AddSingleton<DynamicProxyConfigProvider>();
|
builder.Services.AddSingleton<DynamicProxyConfigProvider>();
|
||||||
builder.Services.AddSingleton<IProxyConfigProvider>(sp => sp.GetRequiredService<DynamicProxyConfigProvider>());
|
builder.Services.AddSingleton<IProxyConfigProvider>(sp => sp.GetRequiredService<DynamicProxyConfigProvider>());
|
||||||
|
|
||||||
|
builder.Services.AddHostedService<PgSqlConfigChangeListener>();
|
||||||
|
|
||||||
var corsSettings = builder.Configuration.GetSection("Cors");
|
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
@ -86,6 +88,9 @@ builder.Services.AddCors(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddHttpForwarder();
|
||||||
|
builder.Services.AddRouting();
|
||||||
|
builder.Services.AddReverseProxy();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@ -20,4 +20,8 @@
|
|||||||
<PackageReference Include="Yarp.ReverseProxy" />
|
<PackageReference Include="Yarp.ReverseProxy" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Fengling.ServiceDiscovery\Fengling.ServiceDiscovery.Core\Fengling.ServiceDiscovery.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Fengling.ServiceDiscovery\Fengling.ServiceDiscovery.Kubernetes\Fengling.ServiceDiscovery.Kubernetes.csproj" />
|
||||||
|
<ProjectReference Include="..\Fengling.ServiceDiscovery\Fengling.ServiceDiscovery.Static\Fengling.ServiceDiscovery.Static.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user