diff --git a/src/Controllers/GlobalUsing.cs b/src/Controllers/GlobalUsing.cs index 737ce24..a2acf50 100644 --- a/src/Controllers/GlobalUsing.cs +++ b/src/Controllers/GlobalUsing.cs @@ -1,5 +1,6 @@ -global using OpenIddict.Validation.AspNetCore; +global using OpenIddict.Validation.AspNetCore; global using Fengling.Console.Models.Dtos; global using Fengling.Console.Services; +global using Fengling.Platform.Infrastructure; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Mvc; \ No newline at end of file diff --git a/src/Controllers/PendingConfigController.cs b/src/Controllers/PendingConfigController.cs new file mode 100644 index 0000000..294a233 --- /dev/null +++ b/src/Controllers/PendingConfigController.cs @@ -0,0 +1,491 @@ +using System.Security.Claims; +using System.Text.Json; +using System.Text.Json.Nodes; +using Fengling.Console.Models.Dtos; +using Fengling.Console.Models.Entities; +using Fengling.Console.Models.K8s; +using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate; + +namespace Fengling.Console.Controllers; + +/// +/// 待确认配置管理控制器 +/// 提供从 K8s 服务发现或手动添加的待确认网关配置的确认、拒绝、修改等功能 +/// +[ApiController] +[Route("api/console/pending-configs")] +[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)] +public class PendingConfigController : ControllerBase +{ + private readonly PendingConfigCache _pendingConfigCache; + private readonly INotificationService _notificationService; + private readonly IClusterStore _clusterStore; + private readonly IRouteStore _routeStore; + private readonly ILogger _logger; + + public PendingConfigController( + PendingConfigCache pendingConfigCache, + INotificationService notificationService, + IClusterStore clusterStore, + IRouteStore routeStore, + ILogger logger) + { + _pendingConfigCache = pendingConfigCache; + _notificationService = notificationService; + _clusterStore = clusterStore; + _routeStore = routeStore; + _logger = logger; + } + + /// + /// 获取待确认配置统计信息 + /// + /// 待确认配置的统计数据,包括总数、新配置数、变更数 + /// 成功返回统计数据 + /// 服务器内部错误 + [HttpGet("statistics")] + [Produces("application/json")] + [ProducesResponseType(typeof(PendingConfigStatisticsDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public ActionResult GetStatistics() + { + try + { + var allConfigs = _pendingConfigCache.GetAll().ToList(); + var result = new PendingConfigStatisticsDto + { + TotalCount = allConfigs.Count, + NewCount = allConfigs.Count(c => c.IsNew), + ModifiedCount = allConfigs.Count(c => c.IsModified) + }; + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting pending config statistics"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取待确认配置列表 + /// 支持分页、按状态筛选和按服务名搜索 + /// + /// 查询参数,包括分页、状态筛选和服务名搜索 + /// 分页的待确认配置列表 + /// 成功返回配置列表 + /// 服务器内部错误 + [HttpGet] + [Produces("application/json")] + [ProducesResponseType(typeof(PagedResultDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public ActionResult> GetList([FromQuery] PendingConfigListRequest request) + { + try + { + var query = _pendingConfigCache.GetAll().AsEnumerable(); + + // 应用状态筛选 + if (request.Status.HasValue) + { + query = query.Where(c => c.Status == request.Status.Value); + } + + // 应用服务名称搜索 + if (!string.IsNullOrWhiteSpace(request.ServiceName)) + { + query = query.Where(c => c.ServiceName.Contains(request.ServiceName, StringComparison.OrdinalIgnoreCase)); + } + + // 应用租户编码筛选 + if (!string.IsNullOrWhiteSpace(request.TenantCode)) + { + query = query.Where(c => c.TenantCode == request.TenantCode); + } + + // 应用来源筛选 + if (request.Source.HasValue) + { + query = query.Where(c => c.Source == request.Source.Value); + } + + var allItems = query.ToList(); + var totalCount = allItems.Count; + + // 应用分页 + var items = allItems + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .Select(MapToDto) + .ToList(); + + var result = new PagedResultDto + { + Items = items, + TotalCount = totalCount, + Page = request.Page, + PageSize = request.PageSize + }; + + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting pending config list"); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 获取单个待确认配置详情 + /// + /// 配置ID + /// 待确认配置的详细信息,包括 JSON 配置内容 + /// 成功返回配置详情 + /// 配置不存在 + /// 服务器内部错误 + [HttpGet("{id}")] + [Produces("application/json")] + [ProducesResponseType(typeof(PendingConfigDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public ActionResult GetDetail(string id) + { + try + { + var config = FindConfigById(id); + if (config == null) + { + return NotFound(new { message = $"Pending config {id} not found" }); + } + + return Ok(MapToDto(config)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting pending config {ConfigId}", id); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 确认待确认配置 + /// 将配置写入数据库(GwTenantRoute/GwCluster/GwDestination),发送 PostgreSQL NOTIFY 通知,并从缓存中移除 + /// + /// 配置ID + /// 确认请求,包含可选的备注信息 + /// 操作结果 + /// 成功确认配置 + /// 配置不存在 + /// 配置格式无效或保存失败 + /// 服务器内部错误 + [HttpPost("{id}/confirm")] + [Authorize(Roles = "Admin")] + [Produces("application/json")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public async Task Confirm(string id, [FromBody] ConfirmPendingConfigRequest? request) + { + try + { + var config = FindConfigById(id); + if (config == null) + { + return NotFound(new { message = $"Pending config {id} not found" }); + } + + // 解析配置 JSON + var configModel = JsonSerializer.Deserialize(config.ConfigJson, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + if (configModel == null) + { + return BadRequest(new { message = "Invalid config format" }); + } + + // 获取当前用户 + var confirmedBy = User.FindFirst(ClaimTypes.Name)?.Value ?? User.Identity?.Name ?? "system"; + + // 写入数据库 + await SaveConfigToDatabaseAsync(configModel); + + // 发送 PostgreSQL NOTIFY + await _notificationService.PublishConfigChangeAsync( + "route", + "create", + new { configId = id, serviceName = config.ServiceName, clusterId = config.ClusterId } + ); + + // 从缓存中移除 + _pendingConfigCache.Remove(config.SourceId); + + _logger.LogInformation( + "Pending config {ConfigId} confirmed by {ConfirmedBy}. Service: {ServiceName}", + id, confirmedBy, config.ServiceName); + + return Ok(new { message = "Config confirmed successfully", configId = id }); + } + catch (JsonException ex) + { + _logger.LogError(ex, "Error parsing config JSON for {ConfigId}", id); + return BadRequest(new { message = "Invalid config JSON format" }); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "Validation error confirming config {ConfigId}", id); + return BadRequest(new { message = ex.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error confirming pending config {ConfigId}", id); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 拒绝待确认配置 + /// 从缓存中移除配置,并可选择记录拒绝原因 + /// + /// 配置ID + /// 拒绝请求,包含可选的拒绝原因 + /// 操作结果 + /// 成功拒绝配置 + /// 配置不存在 + /// 服务器内部错误 + [HttpPost("{id}/reject")] + [Authorize(Roles = "Admin")] + [Produces("application/json")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public ActionResult Reject(string id, [FromBody] RejectPendingConfigRequest? request) + { + try + { + var config = FindConfigById(id); + if (config == null) + { + return NotFound(new { message = $"Pending config {id} not found" }); + } + + // 从缓存中移除 + var removed = _pendingConfigCache.Remove(config.SourceId); + if (!removed) + { + return NotFound(new { message = $"Pending config {id} not found in cache" }); + } + + // 记录拒绝原因(可选) + var reason = request?.Reason ?? "No reason provided"; + _logger.LogInformation( + "Pending config {ConfigId} rejected. Reason: {Reason}. Service: {ServiceName}", + id, reason, config.ServiceName); + + return Ok(new { message = "Config rejected successfully", configId = id, reason }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error rejecting pending config {ConfigId}", id); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 修改待确认配置 + /// 允许用户调整配置内容,更新缓存中的配置,并标记为 Modified 状态 + /// + /// 配置ID + /// 修改请求,包含新的配置内容和修改原因 + /// 更新后的配置详情 + /// 成功修改配置 + /// 配置不存在 + /// 配置格式无效 + /// 服务器内部错误 + [HttpPut("{id}/modify")] + [Authorize(Roles = "Admin")] + [Produces("application/json")] + [ProducesResponseType(typeof(PendingConfigDto), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + public ActionResult Modify(string id, [FromBody] ModifyPendingConfigRequest request) + { + try + { + if (request?.Config == null) + { + return BadRequest(new { message = "Config content is required" }); + } + + var config = FindConfigById(id); + if (config == null) + { + return NotFound(new { message = $"Pending config {id} not found" }); + } + + // 将新的配置内容序列化为 JSON + var newConfigJson = request.Config.ToJsonString(new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // 更新配置 + config.ConfigJson = newConfigJson; + config.Status = PendingConfigStatus.Modified; + config.IsModified = true; + + // 尝试解析配置以更新相关字段 + try + { + var configModel = JsonSerializer.Deserialize(newConfigJson, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + if (configModel != null) + { + config.ServiceName = configModel.ServiceName; + config.TenantCode = configModel.TenantCode; + config.ClusterId = configModel.ClusterId; + } + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Failed to parse modified config for {ConfigId}", id); + // 即使解析失败,仍然保存 JSON 内容 + } + + // 更新缓存 + _pendingConfigCache.AddOrUpdate(config); + + _logger.LogInformation( + "Pending config {ConfigId} modified. Reason: {Reason}. Service: {ServiceName}", + id, request.Reason ?? "No reason provided", config.ServiceName); + + return Ok(MapToDto(config)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error modifying pending config {ConfigId}", id); + return StatusCode(500, new { message = ex.Message }); + } + } + + /// + /// 根据 ID 查找配置 + /// + private PendingConfig? FindConfigById(string id) + { + return _pendingConfigCache.GetAll().FirstOrDefault(c => c.Id == id); + } + + /// + /// 将 PendingConfig 映射为 PendingConfigDto + /// + private static PendingConfigDto MapToDto(PendingConfig config) + { + JsonNode? configNode = null; + try + { + if (!string.IsNullOrWhiteSpace(config.ConfigJson)) + { + configNode = JsonNode.Parse(config.ConfigJson); + } + } + catch (JsonException) + { + // 忽略解析错误,返回 null + } + + return new PendingConfigDto + { + Id = config.Id, + Type = config.Type.ToString(), + Source = config.Source.ToString(), + SourceId = config.SourceId, + Config = configNode, + ServiceName = config.ServiceName, + TenantCode = config.TenantCode, + ClusterId = config.ClusterId, + Status = config.Status.ToString(), + CreatedAt = config.CreatedAt, + ConfirmedAt = config.ConfirmedAt, + ConfirmedBy = config.ConfirmedBy, + IsNew = config.IsNew, + IsModified = config.IsModified + }; + } + + /// + /// 将配置保存到数据库 + /// + private async Task SaveConfigToDatabaseAsync(GatewayConfigModel config) + { + // 1. 创建或更新 Cluster + var cluster = await _clusterStore.FindByClusterIdAsync(config.ClusterId); + if (cluster == null) + { + cluster = new GwCluster + { + Id = Guid.CreateVersion7().ToString("N"), + ClusterId = config.ClusterId, + Name = $"{config.ServiceName} Cluster", + Destinations = new List() + }; + await _clusterStore.CreateAsync(cluster); + _logger.LogInformation("Created new cluster: {ClusterId}", config.ClusterId); + } + + // 2. 添加 Destination 到 Cluster + var serviceAddress = config.ServiceAddress ?? $"http://{config.ServiceName}"; + var port = config.Port ?? 80; + var destinationAddress = $"{serviceAddress}:{port}"; + + var destination = new GwDestination + { + DestinationId = config.DestinationId, + Address = destinationAddress, + Weight = config.Weight, + HealthStatus = 1, // Healthy + Status = 1, // Active + TenantCode = config.TenantCode ?? "" + }; + + await _clusterStore.AddDestinationAsync(config.ClusterId, destination); + _logger.LogInformation( + "Added destination {DestinationId} to cluster {ClusterId}: {Address}", + config.DestinationId, config.ClusterId, destinationAddress); + + // 3. 创建 Route + var pathPattern = string.IsNullOrEmpty(config.PathPrefix) + ? $"/{config.ServiceName}/{{**path}}" + : config.PathPrefix.TrimEnd('/') + "/{**path}"; + + var routeId = Guid.CreateVersion7().ToString("N"); + var route = new GwTenantRoute + { + Id = routeId, + TenantCode = config.IsGlobal ? "" : (config.TenantCode ?? ""), + ServiceName = config.ServiceName, + ClusterId = config.ClusterId, + Match = new GwRouteMatch + { + Path = pathPattern, + Hosts = !string.IsNullOrEmpty(config.Host) ? new List { config.Host } : null + }, + Priority = config.IsGlobal ? 0 : 10, + Status = (int)RouteStatus.Active, + IsGlobal = config.IsGlobal, + CreatedTime = DateTime.UtcNow + }; + + await _routeStore.CreateAsync(route); + _logger.LogInformation( + "Created new route: {RouteId} for service {ServiceName} with path {PathPattern}", + routeId, config.ServiceName, pathPattern); + } +} diff --git a/src/Models/Dtos/PendingConfigDtos.cs b/src/Models/Dtos/PendingConfigDtos.cs new file mode 100644 index 0000000..b91222b --- /dev/null +++ b/src/Models/Dtos/PendingConfigDtos.cs @@ -0,0 +1,176 @@ +using System.Text.Json.Nodes; +using Fengling.Console.Models.Entities; + +namespace Fengling.Console.Models.Dtos; + +/// +/// 待确认配置 DTO +/// 返回给前端的配置详情 +/// +public class PendingConfigDto +{ + /// + /// 唯一标识 + /// + public string Id { get; set; } = ""; + + /// + /// 配置类型 + /// + public string Type { get; set; } = ""; + + /// + /// 配置来源 + /// + public string Source { get; set; } = ""; + + /// + /// 来源标识(如 K8s Service UID) + /// + public string SourceId { get; set; } = ""; + + /// + /// 配置内容(JSON 对象) + /// + public JsonNode? Config { get; set; } + + /// + /// 服务名称 + /// + public string ServiceName { get; set; } = ""; + + /// + /// 租户编码 + /// + public string? TenantCode { get; set; } + + /// + /// 集群ID + /// + public string ClusterId { get; set; } = ""; + + /// + /// 配置状态 + /// + public string Status { get; set; } = ""; + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 确认时间 + /// + public DateTime? ConfirmedAt { get; set; } + + /// + /// 确认人 + /// + public string? ConfirmedBy { get; set; } + + /// + /// 是否为新配置 + /// + public bool IsNew { get; set; } + + /// + /// 是否已修改 + /// + public bool IsModified { get; set; } +} + +/// +/// 待确认配置列表查询请求 +/// +public class PendingConfigListRequest +{ + /// + /// 页码,从 1 开始 + /// + public int Page { get; set; } = 1; + + /// + /// 每页大小,默认 10 + /// + public int PageSize { get; set; } = 10; + + /// + /// 按状态筛选(Pending, Confirmed, Rejected, Modified) + /// + public PendingConfigStatus? Status { get; set; } + + /// + /// 按服务名称搜索(支持模糊匹配) + /// + public string? ServiceName { get; set; } + + /// + /// 按租户编码筛选 + /// + public string? TenantCode { get; set; } + + /// + /// 按配置来源筛选(K8sDiscovery, Manual) + /// + public PendingConfigSource? Source { get; set; } +} + +/// +/// 确认待确认配置请求 +/// +public class ConfirmPendingConfigRequest +{ + /// + /// 确认备注信息 + /// + public string? Remark { get; set; } +} + +/// +/// 拒绝待确认配置请求 +/// +public class RejectPendingConfigRequest +{ + /// + /// 拒绝原因 + /// + public string? Reason { get; set; } +} + +/// +/// 修改待确认配置请求 +/// +public class ModifyPendingConfigRequest +{ + /// + /// 修改后的配置内容(JSON 对象) + /// + public JsonNode? Config { get; set; } + + /// + /// 修改原因 + /// + public string? Reason { get; set; } +} + +/// +/// 待确认配置统计信息 +/// +public class PendingConfigStatisticsDto +{ + /// + /// 待确认总数 + /// + public int TotalCount { get; set; } + + /// + /// 新配置数量 + /// + public int NewCount { get; set; } + + /// + /// 变更配置数量 + /// + public int ModifiedCount { get; set; } +} diff --git a/src/Program.cs b/src/Program.cs index d92e988..bafebfd 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -96,6 +96,9 @@ builder.Services.AddSwaggerGen(c => }); +// 添加通知服务 +builder.Services.AddNotificationService(); + // 添加 K8s 服务监视 builder.Services.AddServiceLabelParser(); builder.Services.AddPendingConfigCache();