IMPL-2: 实现 Service Label 解析器
Some checks are pending
Build and Push Docker / build (push) Waiting to run

- 创建 GatewayConfigModel 数据模型
- 创建 ServiceLabelParser 解析器类
- 实现 Label 到模型字段的映射逻辑
- 添加必需字段验证(ServiceName, ClusterId, PathPrefix, DestinationId)
- 支持批量解析和可选字段提取
- 提供服务地址和端口自动提取功能
This commit is contained in:
movingsam 2026-03-08 00:54:56 +08:00
parent ec85b285bc
commit 5f27300035
2 changed files with 376 additions and 0 deletions

View File

@ -0,0 +1,130 @@
namespace Fengling.Console.Models.K8s;
/// <summary>
/// K8s Service 路由配置模型
/// 从 Service Label 解析得到的网关配置信息
/// </summary>
public sealed class GatewayConfigModel
{
/// <summary>
/// 服务名称(必需)
/// 对应 Label: app-router-name
/// </summary>
public string ServiceName { get; set; } = "";
/// <summary>
/// 集群 ID必需
/// 对应 Label: app-cluster-name
/// </summary>
public string ClusterId { get; set; } = "";
/// <summary>
/// 目标地址 ID必需
/// 对应 Label: app-cluster-destination
/// </summary>
public string DestinationId { get; set; } = "";
/// <summary>
/// 主机地址(可选)
/// 对应 Label: app-router-host
/// </summary>
public string? Host { get; set; }
/// <summary>
/// 路径前缀(必需)
/// 对应 Label: app-router-prefix
/// </summary>
public string PathPrefix { get; set; } = "";
/// <summary>
/// 租户代码(可选)
/// </summary>
public string? TenantCode { get; set; }
/// <summary>
/// 命名空间
/// </summary>
public string Namespace { get; set; } = "";
/// <summary>
/// 服务地址ClusterIP 或 ExternalIP
/// </summary>
public string? ServiceAddress { get; set; }
/// <summary>
/// 服务端口号
/// </summary>
public int? Port { get; set; }
/// <summary>
/// 是否全局路由
/// </summary>
public bool IsGlobal { get; set; } = true;
/// <summary>
/// 权重
/// </summary>
public int Weight { get; set; } = 1;
/// <summary>
/// 版本
/// </summary>
public string Version { get; set; } = "v1";
/// <summary>
/// 原始标签字典
/// </summary>
public IDictionary<string, string> RawLabels { get; set; } = new Dictionary<string, string>();
}
/// <summary>
/// 配置解析结果
/// </summary>
public sealed class GatewayConfigParseResult
{
/// <summary>
/// 是否解析成功
/// </summary>
public bool Success { get; init; }
/// <summary>
/// 解析成功的配置模型
/// </summary>
public GatewayConfigModel? Config { get; init; }
/// <summary>
/// 错误信息列表
/// </summary>
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
/// <summary>
/// 警告信息列表
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
/// <summary>
/// 创建成功结果
/// </summary>
public static GatewayConfigParseResult Succeed(GatewayConfigModel config, IEnumerable<string>? warnings = null)
{
return new GatewayConfigParseResult
{
Success = true,
Config = config,
Warnings = warnings?.ToList() ?? new List<string>()
};
}
/// <summary>
/// 创建失败结果
/// </summary>
public static GatewayConfigParseResult Failed(IEnumerable<string> errors, IEnumerable<string>? warnings = null)
{
return new GatewayConfigParseResult
{
Success = false,
Errors = errors.ToList(),
Warnings = warnings?.ToList() ?? new List<string>()
};
}
}

View File

@ -0,0 +1,246 @@
using Fengling.Console.Models.K8s;
using k8s.Models;
namespace Fengling.Console.Services;
/// <summary>
/// K8s Service Label 解析器
/// 负责从 Service 的 Labels 中解析网关配置信息
/// </summary>
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<ServiceLabelParser> _logger;
public ServiceLabelParser(ILogger<ServiceLabelParser> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 解析 Service 的 Label转换为 GatewayConfigModel
/// </summary>
/// <param name="service">K8s Service 对象</param>
/// <returns>解析结果</returns>
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<string, string>();
var errors = new List<string>();
var warnings = new List<string>();
// 提取必需字段
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<string, string>(labels)
};
_logger.LogDebug("成功解析 Service {ServiceName}/{Namespace} 的 Label 配置",
metadata.Name,
metadata.NamespaceProperty);
return GatewayConfigParseResult.Succeed(config, warnings);
}
/// <summary>
/// 批量解析多个 Service
/// </summary>
/// <param name="services">Service 列表</param>
/// <returns>解析结果列表</returns>
public IEnumerable<GatewayConfigParseResult> ParseMany(IEnumerable<V1Service> services)
{
ArgumentNullException.ThrowIfNull(services);
foreach (var service in services)
{
yield return Parse(service);
}
}
/// <summary>
/// 检查 Service 是否包含必需的网关路由 Label
/// </summary>
/// <param name="service">K8s Service 对象</param>
/// <returns>是否包含必需 Label</returns>
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));
}
/// <summary>
/// 获取 Label 值(不区分大小写)
/// </summary>
private static string? GetLabelValue(IDictionary<string, string> 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();
}
/// <summary>
/// 提取服务地址
/// </summary>
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;
}
/// <summary>
/// 提取服务端口号
/// </summary>
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;
}
}
/// <summary>
/// ServiceLabelParser 扩展方法
/// </summary>
public static class ServiceLabelParserExtensions
{
/// <summary>
/// 添加 ServiceLabelParser 服务
/// </summary>
public static IServiceCollection AddServiceLabelParser(this IServiceCollection services)
{
services.AddSingleton<ServiceLabelParser>();
return services;
}
}