Добавлен модуль Kubernetes MCP с DI и диагностикой ошибок

This commit is contained in:
2026-04-12 23:12:24 +03:00
parent cdbb2110c9
commit ca20a4e7d4
9 changed files with 818 additions and 17 deletions

View File

@@ -0,0 +1,199 @@
using System.ComponentModel;
using System.Text.Json;
using k8s;
using k8s.Autorest;
using k8s.Models;
using ModelContextProtocol.Server;
namespace LazyBear.MCP.Services.Kubernetes;
[McpServerToolType]
public sealed class K8sDeploymentTools(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("Список deployment в namespace")]
public async Task<string> ListDeployments(
[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 deployments = await client.AppsV1.ListNamespacedDeploymentAsync(ns, cancellationToken: cancellationToken);
if (deployments.Items.Count == 0)
{
return $"В namespace '{ns}' deployment не найдены.";
}
var lines = deployments.Items.Select(dep =>
{
var name = dep.Metadata?.Name ?? "unknown";
var desired = dep.Spec?.Replicas ?? 0;
var ready = dep.Status?.ReadyReplicas ?? 0;
var available = dep.Status?.AvailableReplicas ?? 0;
return $"{name}: desired={desired}, ready={ready}, available={available}";
});
return $"Deployment namespace '{ns}':\n{string.Join('\n', lines)}";
}
catch (Exception ex)
{
return FormatError("list_deployments", ns, ex);
}
}
[McpServerTool, Description("Масштабировать deployment")]
public async Task<string> ScaleDeployment(
[Description("Имя deployment")] string name,
[Description("Новое количество реплик")] int replicas,
[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 deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(name, ns, cancellationToken: cancellationToken);
deployment.Spec ??= new V1DeploymentSpec();
deployment.Spec.Replicas = replicas;
await client.AppsV1.ReplaceNamespacedDeploymentAsync(deployment, name, ns, cancellationToken: cancellationToken);
return $"Deployment '{name}' в namespace '{ns}' масштабирован до {replicas} реплик.";
}
catch (Exception ex)
{
return FormatError("scale_deployment", ns, ex, name);
}
}
[McpServerTool, Description("Статус rollout deployment")]
public async Task<string> GetRolloutStatus(
[Description("Имя deployment")] 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 deployment = await client.AppsV1.ReadNamespacedDeploymentStatusAsync(name, ns, cancellationToken: cancellationToken);
var desired = deployment.Spec?.Replicas ?? 0;
var updated = deployment.Status?.UpdatedReplicas ?? 0;
var ready = deployment.Status?.ReadyReplicas ?? 0;
var available = deployment.Status?.AvailableReplicas ?? 0;
return $"Rollout '{name}' в namespace '{ns}': desired={desired}, updated={updated}, ready={ready}, available={available}";
}
catch (Exception ex)
{
return FormatError("get_rollout_status", ns, ex, name);
}
}
[McpServerTool, Description("Перезапустить deployment (rolling restart)")]
public async Task<string> RestartDeployment(
[Description("Имя deployment")] 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 patchJson = JsonSerializer.Serialize(new
{
spec = new
{
template = new
{
metadata = new
{
annotations = new Dictionary<string, string>
{
["kubectl.kubernetes.io/restartedAt"] = DateTimeOffset.UtcNow.ToString("O")
}
}
}
}
});
var patch = new V1Patch(patchJson, V1Patch.PatchType.StrategicMergePatch);
await client.AppsV1.PatchNamespacedDeploymentAsync(patch, name, ns, cancellationToken: cancellationToken);
return $"Deployment '{name}' в namespace '{ns}' отправлен на rolling restart.";
}
catch (Exception ex)
{
return FormatError("restart_deployment", ns, ex, name);
}
}
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}";
}
}