Files
LazyBearWorks/LazyBear.MCP/Services/Kubernetes/K8sNetworkTools.cs

185 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel;
using k8s;
using k8s.Autorest;
using ModelContextProtocol.Server;
namespace LazyBear.MCP.Services.Kubernetes;
[McpServerToolType]
public sealed class K8sNetworkTools(K8sClientProvider clientProvider, IConfiguration configuration)
{
private readonly IKubernetes? _client = clientProvider.Client;
private readonly string? _clientInitializationError = clientProvider.InitializationError;
private readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
[McpServerTool, Description("Список service в namespace")]
public async Task<string> ListServices(
[Description("Namespace Kubernetes")] string? @namespace = null,
CancellationToken cancellationToken = default)
{
var ns = ResolveNamespace(@namespace);
if (!TryGetClient(out var client, out var clientError))
{
return clientError;
}
try
{
var services = await client.CoreV1.ListNamespacedServiceAsync(ns, cancellationToken: cancellationToken);
if (services.Items.Count == 0)
{
return $"В namespace '{ns}' service не найдены.";
}
var lines = services.Items.Select(svc =>
{
var name = svc.Metadata?.Name ?? "unknown";
var type = svc.Spec?.Type ?? "ClusterIP";
var clusterIp = svc.Spec?.ClusterIP ?? "-";
var ports = svc.Spec?.Ports is null
? "-"
: string.Join(",", svc.Spec.Ports.Select(p => $"{p.Port}/{p.Protocol}"));
return $"{name}: type={type}, clusterIp={clusterIp}, ports={ports}";
});
return $"Services namespace '{ns}':\n{string.Join('\n', lines)}";
}
catch (Exception ex)
{
return FormatError("list_services", ns, ex);
}
}
[McpServerTool, Description("Детали service")]
public async Task<string> GetServiceDetails(
[Description("Имя service")] string name,
[Description("Namespace Kubernetes")] string? @namespace = null,
CancellationToken cancellationToken = default)
{
var ns = ResolveNamespace(@namespace);
if (!TryGetClient(out var client, out var clientError))
{
return clientError;
}
try
{
var service = await client.CoreV1.ReadNamespacedServiceAsync(name, ns, cancellationToken: cancellationToken);
var type = service.Spec?.Type ?? "ClusterIP";
var clusterIp = service.Spec?.ClusterIP ?? "-";
var externalIps = service.Spec?.ExternalIPs is null ? "-" : string.Join(",", service.Spec.ExternalIPs);
var selector = service.Spec?.Selector is null
? "-"
: string.Join(",", service.Spec.Selector.Select(x => $"{x.Key}={x.Value}"));
var ports = service.Spec?.Ports is null
? "-"
: string.Join(",", service.Spec.Ports.Select(p => $"{p.Name ?? "port"}:{p.Port}->{p.TargetPort}"));
return $"Service '{name}' namespace '{ns}': type={type}, clusterIp={clusterIp}, externalIps={externalIps}, selector={selector}, ports={ports}";
}
catch (Exception ex)
{
return FormatError("get_service_details", ns, ex, name);
}
}
[McpServerTool, Description("Список ingress в namespace")]
public async Task<string> ListIngresses(
[Description("Namespace Kubernetes")] string? @namespace = null,
CancellationToken cancellationToken = default)
{
var ns = ResolveNamespace(@namespace);
if (!TryGetClient(out var client, out var clientError))
{
return clientError;
}
try
{
var ingresses = await client.NetworkingV1.ListNamespacedIngressAsync(ns, cancellationToken: cancellationToken);
if (ingresses.Items.Count == 0)
{
return $"В namespace '{ns}' ingress не найдены.";
}
var lines = ingresses.Items.Select(ing =>
{
var name = ing.Metadata?.Name ?? "unknown";
var rules = ing.Spec?.Rules is null
? "-"
: string.Join(";", ing.Spec.Rules.Select(r =>
{
var host = string.IsNullOrWhiteSpace(r.Host) ? "*" : r.Host;
var paths = r.Http?.Paths is null
? "-"
: string.Join(",", r.Http.Paths.Select(p =>
{
var serviceName = p.Backend?.Service?.Name ?? "-";
var servicePort = p.Backend?.Service?.Port?.Number?.ToString() ?? p.Backend?.Service?.Port?.Name ?? "-";
return $"{p.Path ?? "/"}->{serviceName}:{servicePort}";
}));
return $"{host}:{paths}";
}));
return $"{name}: rules={rules}";
});
return $"Ingress namespace '{ns}':\n{string.Join('\n', lines)}";
}
catch (Exception ex)
{
return FormatError("list_ingresses", ns, ex);
}
}
private string ResolveNamespace(string? @namespace)
{
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
}
private static string BuildClientInitializationError()
{
return "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT).";
}
private bool TryGetClient(out IKubernetes client, out string error)
{
if (_client is null)
{
client = null!;
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
? string.Empty
: $" Детали: {_clientInitializationError}";
error = BuildClientInitializationError() + details;
return false;
}
client = _client;
error = string.Empty;
return true;
}
private static string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
{
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
if (exception is HttpOperationException httpException)
{
var statusCode = httpException.Response?.StatusCode;
var reason = httpException.Response?.ReasonPhrase;
var body = string.IsNullOrWhiteSpace(httpException.Response?.Content)
? "-"
: httpException.Response!.Content;
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): status={(int?)statusCode ?? 0} {reason}, details={body}";
}
return $"Ошибка Kubernetes в tool '{toolName}' (namespace='{@namespace}'{resourcePart}): {exception.GetType().Name}: {exception.Message}";
}
}