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 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 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 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 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 { ["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}"; } }