185 lines
7.1 KiB
C#
185 lines
7.1 KiB
C#
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}";
|
||
}
|
||
}
|