- Добавлен RazorConsole.Core для интерактивного TUI-дашборда - ToolRegistryService: живое включение/отключение модулей и отдельных методов - InMemoryLogSink: кольцевой буфер логов с фильтрацией по модулю - TUI: 3 таба (Overview, Logs, Settings) - IToolModule: generic-интерфейс для легкого добавления новых MCP-модулей - Guard-проверка TryCheckEnabled() во всех существующих инструментах
126 lines
5.5 KiB
C#
126 lines
5.5 KiB
C#
using System.Text.RegularExpressions;
|
|
using k8s;
|
|
using LazyBear.MCP.Services.ToolRegistry;
|
|
using Microsoft.Extensions.Logging;
|
|
using ModelContextProtocol.Server;
|
|
|
|
namespace LazyBear.MCP.Services.Kubernetes;
|
|
|
|
[McpServerToolType]
|
|
public abstract class KubernetesToolsBase(
|
|
K8sClientProvider clientProvider,
|
|
IConfiguration configuration,
|
|
ToolRegistryService registry,
|
|
ILogger? logger = null)
|
|
{
|
|
protected readonly IKubernetes? _client = clientProvider.Client;
|
|
protected readonly string? _clientInitializationError = clientProvider.InitializationError;
|
|
protected readonly string _defaultNamespace = configuration["Kubernetes:DefaultNamespace"] ?? "default";
|
|
protected readonly ILogger? _logger = logger;
|
|
protected readonly ToolRegistryService _registry = registry;
|
|
|
|
protected const string K8sModuleName = "Kubernetes";
|
|
|
|
protected bool TryCheckEnabled(string toolName, out string error)
|
|
{
|
|
if (!_registry.IsToolEnabled(K8sModuleName, toolName))
|
|
{
|
|
error = $"Инструмент '{toolName}' модуля Kubernetes отключён в TUI.";
|
|
return false;
|
|
}
|
|
error = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
protected static readonly Regex NamespaceRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
|
protected static readonly Regex ResourceNameRegex = new(@"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
|
|
|
protected string ResolveNamespace(string? @namespace)
|
|
{
|
|
return string.IsNullOrWhiteSpace(@namespace) ? _defaultNamespace : @namespace;
|
|
}
|
|
|
|
protected void ValidateNamespace(string value, string parameterName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
throw new ArgumentException($"Namespace не может быть пустым", parameterName);
|
|
}
|
|
if (value.Length > 63)
|
|
{
|
|
throw new ArgumentException($"Namespace не может быть длиннее 63 символов", parameterName);
|
|
}
|
|
if (!NamespaceRegex.IsMatch(value))
|
|
{
|
|
throw new ArgumentException($"Невалидный формат namespace: '{value}'. Только lowercase alphanumetic, -, не может начинаться/заканчиваться на -", parameterName);
|
|
}
|
|
}
|
|
|
|
protected void ValidateResourceName(string value, string parameterName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
throw new ArgumentException($"Resource name не может быть пустым", parameterName);
|
|
}
|
|
if (value.Length > 253)
|
|
{
|
|
throw new ArgumentException($"Resource name не может быть длиннее 253 символов", parameterName);
|
|
}
|
|
if (!ResourceNameRegex.IsMatch(value))
|
|
{
|
|
throw new ArgumentException($"Невалидный формат: '{value}'. Только lowercase alphanumetic, -, не может начинаться/заканчиваться на -", parameterName);
|
|
}
|
|
}
|
|
|
|
protected bool TryGetClient(out IKubernetes client, out string error)
|
|
{
|
|
if (_client is null)
|
|
{
|
|
client = null!;
|
|
var details = string.IsNullOrWhiteSpace(_clientInitializationError)
|
|
? string.Empty
|
|
: $" Детали: {_clientInitializationError}";
|
|
error = "Kubernetes клиент не инициализирован. Проверьте Kubernetes:KubeconfigPath или in-cluster окружение (KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT)." + details;
|
|
return false;
|
|
}
|
|
|
|
client = _client;
|
|
error = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
protected string FormatError(string toolName, string @namespace, Exception exception, string? resourceName = null)
|
|
{
|
|
_logger?.LogError(exception, "Ошибка Kubernetes в tool '{ToolName}' (namespace='{Namespace}'{ResourcePart}): {ExceptionMessage}",
|
|
toolName, @namespace,
|
|
string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'",
|
|
exception.Message);
|
|
|
|
var resourcePart = string.IsNullOrWhiteSpace(resourceName) ? string.Empty : $", resource='{resourceName}'";
|
|
|
|
if (exception is global::k8s.Autorest.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}";
|
|
}
|
|
|
|
protected string FormatException(string toolName, Exception exception, string? resource = null)
|
|
{
|
|
_logger?.LogError(exception, "Ошибка exception в tool '{ToolName}'{ResourcePart}: {ExceptionMessage}",
|
|
toolName,
|
|
string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'",
|
|
exception.Message);
|
|
|
|
var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'";
|
|
return $"Ошибка в tool '{toolName}'{resourcePart}: {exception.GetType().Name}: {exception.Message}";
|
|
}
|
|
}
|