diff --git a/src/Models/K8s/GatewayConfigModel.cs b/src/Models/K8s/GatewayConfigModel.cs new file mode 100644 index 0000000..59a02ea --- /dev/null +++ b/src/Models/K8s/GatewayConfigModel.cs @@ -0,0 +1,130 @@ +namespace Fengling.Console.Models.K8s; + +/// +/// K8s Service 路由配置模型 +/// 从 Service Label 解析得到的网关配置信息 +/// +public sealed class GatewayConfigModel +{ + /// + /// 服务名称(必需) + /// 对应 Label: app-router-name + /// + public string ServiceName { get; set; } = ""; + + /// + /// 集群 ID(必需) + /// 对应 Label: app-cluster-name + /// + public string ClusterId { get; set; } = ""; + + /// + /// 目标地址 ID(必需) + /// 对应 Label: app-cluster-destination + /// + public string DestinationId { get; set; } = ""; + + /// + /// 主机地址(可选) + /// 对应 Label: app-router-host + /// + public string? Host { get; set; } + + /// + /// 路径前缀(必需) + /// 对应 Label: app-router-prefix + /// + public string PathPrefix { get; set; } = ""; + + /// + /// 租户代码(可选) + /// + public string? TenantCode { get; set; } + + /// + /// 命名空间 + /// + public string Namespace { get; set; } = ""; + + /// + /// 服务地址(ClusterIP 或 ExternalIP) + /// + public string? ServiceAddress { get; set; } + + /// + /// 服务端口号 + /// + public int? Port { get; set; } + + /// + /// 是否全局路由 + /// + public bool IsGlobal { get; set; } = true; + + /// + /// 权重 + /// + public int Weight { get; set; } = 1; + + /// + /// 版本 + /// + public string Version { get; set; } = "v1"; + + /// + /// 原始标签字典 + /// + public IDictionary RawLabels { get; set; } = new Dictionary(); +} + +/// +/// 配置解析结果 +/// +public sealed class GatewayConfigParseResult +{ + /// + /// 是否解析成功 + /// + public bool Success { get; init; } + + /// + /// 解析成功的配置模型 + /// + public GatewayConfigModel? Config { get; init; } + + /// + /// 错误信息列表 + /// + public IReadOnlyList Errors { get; init; } = Array.Empty(); + + /// + /// 警告信息列表 + /// + public IReadOnlyList Warnings { get; init; } = Array.Empty(); + + /// + /// 创建成功结果 + /// + public static GatewayConfigParseResult Succeed(GatewayConfigModel config, IEnumerable? warnings = null) + { + return new GatewayConfigParseResult + { + Success = true, + Config = config, + Warnings = warnings?.ToList() ?? new List() + }; + } + + /// + /// 创建失败结果 + /// + public static GatewayConfigParseResult Failed(IEnumerable errors, IEnumerable? warnings = null) + { + return new GatewayConfigParseResult + { + Success = false, + Errors = errors.ToList(), + Warnings = warnings?.ToList() ?? new List() + }; + } +} diff --git a/src/Services/ServiceLabelParser.cs b/src/Services/ServiceLabelParser.cs new file mode 100644 index 0000000..41bfcae --- /dev/null +++ b/src/Services/ServiceLabelParser.cs @@ -0,0 +1,246 @@ +using Fengling.Console.Models.K8s; +using k8s.Models; + +namespace Fengling.Console.Services; + +/// +/// K8s Service Label 解析器 +/// 负责从 Service 的 Labels 中解析网关配置信息 +/// +public sealed class ServiceLabelParser +{ + // Label 键名常量 + private const string LabelHost = "app-router-host"; + private const string LabelServiceName = "app-router-name"; + private const string LabelPathPrefix = "app-router-prefix"; + private const string LabelClusterId = "app-cluster-name"; + private const string LabelDestinationId = "app-cluster-destination"; + private const string LabelTenantCode = "app-router-tenant"; + private const string LabelVersion = "app-router-version"; + private const string LabelWeight = "app-router-weight"; + + private readonly ILogger _logger; + + public ServiceLabelParser(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// 解析 Service 的 Label,转换为 GatewayConfigModel + /// + /// K8s Service 对象 + /// 解析结果 + public GatewayConfigParseResult Parse(V1Service service) + { + ArgumentNullException.ThrowIfNull(service); + + var metadata = service.Metadata; + if (metadata == null) + { + return GatewayConfigParseResult.Failed(new[] { "Service Metadata 为空" }); + } + + var labels = metadata.Labels ?? new Dictionary(); + var errors = new List(); + var warnings = new List(); + + // 提取必需字段 + var serviceName = GetLabelValue(labels, LabelServiceName); + var clusterId = GetLabelValue(labels, LabelClusterId); + var pathPrefix = GetLabelValue(labels, LabelPathPrefix); + var destinationId = GetLabelValue(labels, LabelDestinationId); + + // 验证必需字段 + if (string.IsNullOrWhiteSpace(serviceName)) + { + errors.Add($"缺少必需的 Label: {LabelServiceName}"); + } + + if (string.IsNullOrWhiteSpace(clusterId)) + { + errors.Add($"缺少必需的 Label: {LabelClusterId}"); + } + + if (string.IsNullOrWhiteSpace(pathPrefix)) + { + errors.Add($"缺少必需的 Label: {LabelPathPrefix}"); + } + + if (string.IsNullOrWhiteSpace(destinationId)) + { + errors.Add($"缺少必需的 Label: {LabelDestinationId}"); + } + + // 如果有验证错误,返回失败结果 + if (errors.Count > 0) + { + _logger.LogWarning("Service {ServiceName}/{Namespace} Label 验证失败: {Errors}", + metadata.Name, + metadata.NamespaceProperty, + string.Join("; ", errors)); + + return GatewayConfigParseResult.Failed(errors, warnings); + } + + // 提取可选字段 + var host = GetLabelValue(labels, LabelHost); + var tenantCode = GetLabelValue(labels, LabelTenantCode); + var version = GetLabelValue(labels, LabelVersion) ?? "v1"; + + // 解析权重 + var weight = 1; + var weightStr = GetLabelValue(labels, LabelWeight); + if (!string.IsNullOrWhiteSpace(weightStr) && !int.TryParse(weightStr, out weight)) + { + warnings.Add($"Label {LabelWeight} 值 '{weightStr}' 不是有效的整数,使用默认值 1"); + weight = 1; + } + + // 获取服务地址 + var serviceAddress = ExtractServiceAddress(service); + var port = ExtractServicePort(service); + + // 构建配置模型 + var config = new GatewayConfigModel + { + ServiceName = serviceName!, + ClusterId = clusterId!, + DestinationId = destinationId!, + Host = host, + PathPrefix = pathPrefix!, + TenantCode = tenantCode, + Namespace = metadata.NamespaceProperty ?? "", + ServiceAddress = serviceAddress, + Port = port, + Version = version, + Weight = weight, + IsGlobal = string.IsNullOrWhiteSpace(tenantCode), + RawLabels = new Dictionary(labels) + }; + + _logger.LogDebug("成功解析 Service {ServiceName}/{Namespace} 的 Label 配置", + metadata.Name, + metadata.NamespaceProperty); + + return GatewayConfigParseResult.Succeed(config, warnings); + } + + /// + /// 批量解析多个 Service + /// + /// Service 列表 + /// 解析结果列表 + public IEnumerable ParseMany(IEnumerable services) + { + ArgumentNullException.ThrowIfNull(services); + + foreach (var service in services) + { + yield return Parse(service); + } + } + + /// + /// 检查 Service 是否包含必需的网关路由 Label + /// + /// K8s Service 对象 + /// 是否包含必需 Label + public bool HasRequiredLabels(V1Service service) + { + ArgumentNullException.ThrowIfNull(service); + + var metadata = service.Metadata; + if (metadata?.Labels == null) + { + return false; + } + + var labels = metadata.Labels; + + // 检查所有必需 Label 是否存在且不为空 + return !string.IsNullOrWhiteSpace(GetLabelValue(labels, LabelServiceName)) + && !string.IsNullOrWhiteSpace(GetLabelValue(labels, LabelClusterId)) + && !string.IsNullOrWhiteSpace(GetLabelValue(labels, LabelPathPrefix)) + && !string.IsNullOrWhiteSpace(GetLabelValue(labels, LabelDestinationId)); + } + + /// + /// 获取 Label 值(不区分大小写) + /// + private static string? GetLabelValue(IDictionary labels, string key) + { + if (labels.TryGetValue(key, out var value)) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + // 尝试不区分大小写查找 + var match = labels.FirstOrDefault(kvp => + kvp.Key.Equals(key, StringComparison.OrdinalIgnoreCase)); + + return string.IsNullOrWhiteSpace(match.Value) ? null : match.Value.Trim(); + } + + /// + /// 提取服务地址 + /// + private static string? ExtractServiceAddress(V1Service service) + { + var spec = service.Spec; + if (spec == null) + { + return null; + } + + // 优先使用 ClusterIP(如果不是 None) + if (!string.IsNullOrWhiteSpace(spec.ClusterIP) && spec.ClusterIP != "None") + { + return spec.ClusterIP; + } + + // 其次使用 ExternalIP + if (spec.ExternalIPs?.Count > 0) + { + return spec.ExternalIPs[0]; + } + + // 最后使用 ExternalName + if (!string.IsNullOrWhiteSpace(spec.ExternalName)) + { + return spec.ExternalName; + } + + return null; + } + + /// + /// 提取服务端口号 + /// + private static int? ExtractServicePort(V1Service service) + { + var spec = service.Spec; + if (spec?.Ports == null || spec.Ports.Count == 0) + { + return null; + } + + // 优先返回第一个端口的端口号 + return spec.Ports[0].Port; + } +} + +/// +/// ServiceLabelParser 扩展方法 +/// +public static class ServiceLabelParserExtensions +{ + /// + /// 添加 ServiceLabelParser 服务 + /// + public static IServiceCollection AddServiceLabelParser(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +}