IMPL-2: 实现 Service Label 解析器
Some checks are pending
Build and Push Docker / build (push) Waiting to run
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:
parent
ec85b285bc
commit
5f27300035
130
src/Models/K8s/GatewayConfigModel.cs
Normal file
130
src/Models/K8s/GatewayConfigModel.cs
Normal 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>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/Services/ServiceLabelParser.cs
Normal file
246
src/Services/ServiceLabelParser.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user