diff --git a/README.md b/README.md index ffd12a25..783e9786 100644 --- a/README.md +++ b/README.md @@ -354,40 +354,14 @@ In case multi-cluster support is enabled (default) and you have access to multip - **kiali_mesh_status** - Get the status of mesh components including Istio, Kiali, Grafana, Prometheus and their interactions, versions, and health status -- **kiali_istio_config** - Get all Istio configuration objects in the mesh including their full YAML resources and details - -- **kiali_istio_object_details** - Get detailed information about a specific Istio object including validation and help information - - `group` (`string`) **(required)** - API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io') - - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway') - - `name` (`string`) **(required)** - Name of the Istio object - - `namespace` (`string`) **(required)** - Namespace containing the Istio object - - `version` (`string`) **(required)** - API version of the Istio object (e.g., 'v1', 'v1beta1') - -- **kiali_istio_object_patch** - Modify an existing Istio object using PATCH method. The JSON patch data will be applied to the existing object. - - `group` (`string`) **(required)** - API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io') - - `json_patch` (`string`) **(required)** - JSON patch data to apply to the object - - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway') - - `name` (`string`) **(required)** - Name of the Istio object - - `namespace` (`string`) **(required)** - Namespace containing the Istio object - - `version` (`string`) **(required)** - API version of the Istio object (e.g., 'v1', 'v1beta1') - -- **kiali_istio_object_create** - Create a new Istio object using POST method. The JSON data will be used to create the new object. - - `group` (`string`) **(required)** - API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io') - - `json_data` (`string`) **(required)** - JSON data for the new object - - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway') - - `namespace` (`string`) **(required)** - Namespace where the Istio object will be created - - `version` (`string`) **(required)** - API version of the Istio object (e.g., 'v1', 'v1beta1') - -- **kiali_istio_object_delete** - Delete an existing Istio object using DELETE method. - - `group` (`string`) **(required)** - API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io') - - `kind` (`string`) **(required)** - Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway') - - `name` (`string`) **(required)** - Name of the Istio object - - `namespace` (`string`) **(required)** - Namespace containing the Istio object - - `version` (`string`) **(required)** - API version of the Istio object (e.g., 'v1', 'v1beta1') - -- **kiali_validations_list** - List all the validations in the current cluster from all namespaces - - `namespace` (`string`) - Optional single namespace to retrieve validations from (alternative to namespaces) - - `namespaces` (`string`) - Optional comma-separated list of namespaces to retrieve validations from +- **kiali_manage_istio_config** - Manages Istio configuration objects (Gateways, VirtualServices, etc.). Can list (objects and validations), get, create, patch, or delete objects + - `action` (`string`) **(required)** - Action to perform: list, get, create, patch, or delete + - `group` (`string`) - API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io') + - `json_data` (`string`) - JSON data to apply or create the object + - `kind` (`string`) - Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway') + - `name` (`string`) - Name of the Istio object + - `namespace` (`string`) - Namespace containing the Istio object + - `version` (`string`) - API version of the Istio object (e.g., 'v1', 'v1beta1') - **kiali_namespaces** - Get all namespaces in the mesh that the user has access to diff --git a/pkg/kiali/endpoints.go b/pkg/kiali/endpoints.go index 1c4c3938..2dffbd7b 100644 --- a/pkg/kiali/endpoints.go +++ b/pkg/kiali/endpoints.go @@ -23,5 +23,4 @@ const ( WorkloadDetailsEndpoint = "/api/namespaces/%s/workloads/%s" WorkloadMetricsEndpoint = "/api/namespaces/%s/workloads/%s/metrics" ValidationsEndpoint = "/api/istio/validations" - ValidationsListEndpoint = "/api/istio/validations" ) diff --git a/pkg/kiali/istio.go b/pkg/kiali/istio.go index bd831d6c..caadab0f 100644 --- a/pkg/kiali/istio.go +++ b/pkg/kiali/istio.go @@ -2,151 +2,96 @@ package kiali import ( "context" + "encoding/json" "fmt" "net/http" "net/url" "strings" + "sync" ) -// IstioConfig calls the Kiali Istio config API to get all Istio objects in the mesh. -// Returns the full YAML resources and additional details about each object. -func (k *Kiali) IstioConfig(ctx context.Context) (string, error) { - endpoint := IstioConfigEndpoint + "?validate=true" - - return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) +type IstioConfigListResponse struct { + Configs json.RawMessage `json:"configs"` + Validations json.RawMessage `json:"validations"` } -// IstioObjectDetails returns detailed information about a specific Istio object. -// Parameters: -// - namespace: the namespace containing the Istio object -// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io") -// - version: the API version (e.g., "v1", "v1beta1") -// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute") -// - name: the name of the resource -func (k *Kiali) IstioObjectDetails(ctx context.Context, namespace, group, version, kind, name string) (string, error) { - if namespace == "" { - return "", fmt.Errorf("namespace is required") - } - if group == "" { - return "", fmt.Errorf("group is required") - } - if version == "" { - return "", fmt.Errorf("version is required") - } - if kind == "" { - return "", fmt.Errorf("kind is required") - } - if name == "" { - return "", fmt.Errorf("name is required") - } - endpoint := fmt.Sprintf(IstioObjectEndpoint+"?validate=true&help=true", +// Helper builders to avoid repeated url.PathEscape boilerplate +func buildIstioObjectEndpoint(namespace, group, version, kind, name string) string { + return fmt.Sprintf(IstioObjectEndpoint, url.PathEscape(namespace), url.PathEscape(group), url.PathEscape(version), url.PathEscape(kind), - url.PathEscape(name)) - - return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) + url.PathEscape(name), + ) } -// IstioObjectPatch patches an existing Istio object using PATCH method. -// Parameters: -// - namespace: the namespace containing the Istio object -// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io") -// - version: the API version (e.g., "v1", "v1beta1") -// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute") -// - name: the name of the resource -// - jsonPatch: the JSON patch data to apply -func (k *Kiali) IstioObjectPatch(ctx context.Context, namespace, group, version, kind, name, jsonPatch string) (string, error) { - if namespace == "" { - return "", fmt.Errorf("namespace is required") - } - if group == "" { - return "", fmt.Errorf("group is required") - } - if version == "" { - return "", fmt.Errorf("version is required") - } - if kind == "" { - return "", fmt.Errorf("kind is required") - } - if name == "" { - return "", fmt.Errorf("name is required") - } - if jsonPatch == "" { - return "", fmt.Errorf("json patch data is required") - } - endpoint := fmt.Sprintf(IstioObjectEndpoint, +func buildIstioObjectCreateEndpoint(namespace, group, version, kind string) string { + return fmt.Sprintf(IstioObjectCreateEndpoint, url.PathEscape(namespace), url.PathEscape(group), url.PathEscape(version), url.PathEscape(kind), - url.PathEscape(name)) - - return k.executeRequest(ctx, http.MethodPatch, endpoint, "application/json", strings.NewReader(jsonPatch)) + ) } -// IstioObjectCreate creates a new Istio object using POST method. -// Parameters: -// - namespace: the namespace where the Istio object will be created -// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io") -// - version: the API version (e.g., "v1", "v1beta1") -// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute") -// - jsonData: the JSON data for the new object -func (k *Kiali) IstioObjectCreate(ctx context.Context, namespace, group, version, kind, jsonData string) (string, error) { - if namespace == "" { - return "", fmt.Errorf("namespace is required") - } - if group == "" { - return "", fmt.Errorf("group is required") - } - if version == "" { - return "", fmt.Errorf("version is required") - } - if kind == "" { - return "", fmt.Errorf("kind is required") - } - if jsonData == "" { - return "", fmt.Errorf("json data is required") - } - endpoint := fmt.Sprintf(IstioObjectCreateEndpoint, - url.PathEscape(namespace), - url.PathEscape(group), - url.PathEscape(version), - url.PathEscape(kind)) +// IstioConfig calls the Kiali Istio config API to get all Istio objects in the mesh. +// Returns the full YAML resources and additional details about each object. +func (k *Kiali) IstioConfig(ctx context.Context, action string, namespace string, group string, version string, kind string, name string, jsonData string) (string, error) { + switch action { + case "get": + endpoint := buildIstioObjectEndpoint(namespace, group, version, kind, name) + "?validate=true&help=true" + return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) + case "create": + endpoint := buildIstioObjectCreateEndpoint(namespace, group, version, kind) + return k.executeRequest(ctx, http.MethodPost, endpoint, "application/json", strings.NewReader(jsonData)) + case "patch": + endpoint := buildIstioObjectEndpoint(namespace, group, version, kind, name) + return k.executeRequest(ctx, http.MethodPatch, endpoint, "application/json", strings.NewReader(jsonData)) + case "delete": + endpoint := buildIstioObjectEndpoint(namespace, group, version, kind, name) + return k.executeRequest(ctx, http.MethodDelete, endpoint, "", nil) + default: + var wg sync.WaitGroup + wg.Add(2) + var configsContent string + var configsErr error + var validationsContent string + var validationsErr error - return k.executeRequest(ctx, http.MethodPost, endpoint, "application/json", strings.NewReader(jsonData)) -} + // List configs (existing list behavior) + go func() { + defer wg.Done() + endpoint := IstioConfigEndpoint + "?validate=true" + configsContent, configsErr = k.executeRequest(ctx, http.MethodGet, endpoint, "", nil) + }() -// IstioObjectDelete deletes an existing Istio object using DELETE method. -// Parameters: -// - namespace: the namespace containing the Istio object -// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io") -// - version: the API version (e.g., "v1", "v1beta1") -// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute", "Gateway") -// - name: the name of the resource -func (k *Kiali) IstioObjectDelete(ctx context.Context, namespace, group, version, kind, name string) (string, error) { - if namespace == "" { - return "", fmt.Errorf("namespace is required") - } - if group == "" { - return "", fmt.Errorf("group is required") - } - if version == "" { - return "", fmt.Errorf("version is required") - } - if kind == "" { - return "", fmt.Errorf("kind is required") - } - if name == "" { - return "", fmt.Errorf("name is required") - } - endpoint := fmt.Sprintf(IstioObjectEndpoint, - url.PathEscape(namespace), - url.PathEscape(group), - url.PathEscape(version), - url.PathEscape(kind), - url.PathEscape(name)) + // List validations, optionally scoped to provided namespace + go func() { + defer wg.Done() + var namespaces []string + if ns := strings.TrimSpace(namespace); ns != "" { + namespaces = []string{ns} + } + validationsContent, validationsErr = k.ValidationsList(ctx, namespaces) + }() - return k.executeRequest(ctx, http.MethodDelete, endpoint, "", nil) + wg.Wait() + if configsErr != nil { + return "", configsErr + } + if validationsErr != nil { + return "", validationsErr + } + + resp := IstioConfigListResponse{ + Configs: json.RawMessage([]byte(configsContent)), + Validations: json.RawMessage([]byte(validationsContent)), + } + out, err := json.Marshal(resp) + if err != nil { + return "", fmt.Errorf("failed to marshal istio list response: %v", err) + } + return string(out), nil + } } diff --git a/pkg/toolsets/kiali/istio_config.go b/pkg/toolsets/kiali/istio_config.go deleted file mode 100644 index 79fb75c5..00000000 --- a/pkg/toolsets/kiali/istio_config.go +++ /dev/null @@ -1,288 +0,0 @@ -package kiali - -import ( - "fmt" - - "github.com/google/jsonschema-go/jsonschema" - "k8s.io/utils/ptr" - - "github.com/containers/kubernetes-mcp-server/pkg/api" -) - -func initIstioConfig() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_istio_config", - Description: "Get all Istio configuration objects in the mesh including their full YAML resources and details", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{}, - Required: []string{}, - }, - Annotations: api.ToolAnnotations{ - Title: "Istio Config: List All", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: istioConfigHandler, - }) - return ret -} - -func istioConfigHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - k := params.NewKiali() - content, err := k.IstioConfig(params.Context) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to retrieve Istio configuration: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func initIstioObjectDetails() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_istio_object_details", - Description: "Get detailed information about a specific Istio object including validation and help information", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the Istio object", - }, - "group": { - Type: "string", - Description: "API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io')", - }, - "version": { - Type: "string", - Description: "API version of the Istio object (e.g., 'v1', 'v1beta1')", - }, - "kind": { - Type: "string", - Description: "Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway')", - }, - "name": { - Type: "string", - Description: "Name of the Istio object", - }, - }, - Required: []string{"namespace", "group", "version", "kind", "name"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Istio Object: Details", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: istioObjectDetailsHandler, - }) - return ret -} - -func istioObjectDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - group, _ := params.GetArguments()["group"].(string) - version, _ := params.GetArguments()["version"].(string) - kind, _ := params.GetArguments()["kind"].(string) - name, _ := params.GetArguments()["name"].(string) - - k := params.NewKiali() - content, err := k.IstioObjectDetails(params.Context, namespace, group, version, kind, name) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to retrieve Istio object details: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func initIstioObjectPatch() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_istio_object_patch", - Description: "Modify an existing Istio object using PATCH method. The JSON patch data will be applied to the existing object.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the Istio object", - }, - "group": { - Type: "string", - Description: "API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io')", - }, - "version": { - Type: "string", - Description: "API version of the Istio object (e.g., 'v1', 'v1beta1')", - }, - "kind": { - Type: "string", - Description: "Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway')", - }, - "name": { - Type: "string", - Description: "Name of the Istio object", - }, - "json_patch": { - Type: "string", - Description: "JSON patch data to apply to the object", - }, - }, - Required: []string{"namespace", "group", "version", "kind", "name", "json_patch"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Istio Object: Patch", - ReadOnlyHint: ptr.To(false), - DestructiveHint: ptr.To(true), - IdempotentHint: ptr.To(false), - OpenWorldHint: ptr.To(false), - }, - }, Handler: istioObjectPatchHandler, - }) - return ret -} - -func istioObjectPatchHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - group, _ := params.GetArguments()["group"].(string) - version, _ := params.GetArguments()["version"].(string) - kind, _ := params.GetArguments()["kind"].(string) - name, _ := params.GetArguments()["name"].(string) - jsonPatch, _ := params.GetArguments()["json_patch"].(string) - - k := params.NewKiali() - content, err := k.IstioObjectPatch(params.Context, namespace, group, version, kind, name, jsonPatch) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to patch Istio object: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func initIstioObjectCreate() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_istio_object_create", - Description: "Create a new Istio object using POST method. The JSON data will be used to create the new object.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace where the Istio object will be created", - }, - "group": { - Type: "string", - Description: "API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io')", - }, - "version": { - Type: "string", - Description: "API version of the Istio object (e.g., 'v1', 'v1beta1')", - }, - "kind": { - Type: "string", - Description: "Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway')", - }, - "json_data": { - Type: "string", - Description: "JSON data for the new object", - }, - }, - Required: []string{"namespace", "group", "version", "kind", "json_data"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Istio Object: Create", - ReadOnlyHint: ptr.To(false), - DestructiveHint: ptr.To(true), - IdempotentHint: ptr.To(false), - OpenWorldHint: ptr.To(false), - }, - }, Handler: istioObjectCreateHandler, - }) - return ret -} - -func istioObjectCreateHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - group, _ := params.GetArguments()["group"].(string) - version, _ := params.GetArguments()["version"].(string) - kind, _ := params.GetArguments()["kind"].(string) - jsonData, _ := params.GetArguments()["json_data"].(string) - - k := params.NewKiali() - content, err := k.IstioObjectCreate(params.Context, namespace, group, version, kind, jsonData) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to create Istio object: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func initIstioObjectDelete() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_istio_object_delete", - Description: "Delete an existing Istio object using DELETE method.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the Istio object", - }, - "group": { - Type: "string", - Description: "API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io')", - }, - "version": { - Type: "string", - Description: "API version of the Istio object (e.g., 'v1', 'v1beta1')", - }, - "kind": { - Type: "string", - Description: "Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway')", - }, - "name": { - Type: "string", - Description: "Name of the Istio object", - }, - }, - Required: []string{"namespace", "group", "version", "kind", "name"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Istio Object: Delete", - ReadOnlyHint: ptr.To(false), - DestructiveHint: ptr.To(true), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(false), - }, - }, Handler: istioObjectDeleteHandler, - }) - return ret -} - -func istioObjectDeleteHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - group, _ := params.GetArguments()["group"].(string) - version, _ := params.GetArguments()["version"].(string) - kind, _ := params.GetArguments()["kind"].(string) - name, _ := params.GetArguments()["name"].(string) - - k := params.NewKiali() - content, err := k.IstioObjectDelete(params.Context, namespace, group, version, kind, name) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to delete Istio object: %v", err)), nil - } - - return api.NewToolCallResult(content, nil), nil -} diff --git a/pkg/toolsets/kiali/manage_istio_config.go b/pkg/toolsets/kiali/manage_istio_config.go new file mode 100644 index 00000000..f9d165fd --- /dev/null +++ b/pkg/toolsets/kiali/manage_istio_config.go @@ -0,0 +1,117 @@ +package kiali + +import ( + "fmt" + + "github.com/google/jsonschema-go/jsonschema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" +) + +func initManageIstioConfig() []api.ServerTool { + ret := make([]api.ServerTool, 0) + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "kiali_manage_istio_config", + Description: "Manages Istio configuration objects (Gateways, VirtualServices, etc.). Can list (objects and validations), get, create, patch, or delete objects", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "action": { + Type: "string", + Description: "Action to perform: list, get, create, patch, or delete", + }, + "namespace": { + Type: "string", + Description: "Namespace containing the Istio object", + }, + "group": { + Type: "string", + Description: "API group of the Istio object (e.g., 'networking.istio.io', 'gateway.networking.k8s.io')", + }, + "version": { + Type: "string", + Description: "API version of the Istio object (e.g., 'v1', 'v1beta1')", + }, + "kind": { + Type: "string", + Description: "Kind of the Istio object (e.g., 'DestinationRule', 'VirtualService', 'HTTPRoute', 'Gateway')", + }, + "name": { + Type: "string", + Description: "Name of the Istio object", + }, + "json_data": { + Type: "string", + Description: "JSON data to apply or create the object", + }, + }, + Required: []string{"action"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Manage Istio Config: List, Get, Create, Patch, Delete", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: istioConfigHandler, + }) + return ret +} + +func istioConfigHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + action, _ := params.GetArguments()["action"].(string) + namespace, _ := params.GetArguments()["namespace"].(string) + group, _ := params.GetArguments()["group"].(string) + version, _ := params.GetArguments()["version"].(string) + kind, _ := params.GetArguments()["kind"].(string) + name, _ := params.GetArguments()["name"].(string) + jsonData, _ := params.GetArguments()["json_data"].(string) + if err := validateIstioConfigInput(action, namespace, group, version, kind, name, jsonData); err != nil { + return api.NewToolCallResult("", err), nil + } + k := params.NewKiali() + content, err := k.IstioConfig(params.Context, action, namespace, group, version, kind, name, jsonData) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to retrieve Istio configuration: %v", err)), nil + } + return api.NewToolCallResult(content, nil), nil +} + +// validateIstioConfigInput centralizes validation rules for manage istio config tool. +// Rules: +// - If action is not "list": namespace, group, version, kind are required +// - If action is "create": name and json_data are required +// - If action is "patch": json_data is required +func validateIstioConfigInput(action, namespace, group, version, kind, name, jsonData string) error { + if action != "list" { + if namespace == "" { + return fmt.Errorf("namespace is required for action %q", action) + } + if group == "" { + return fmt.Errorf("group is required for action %q", action) + } + if version == "" { + return fmt.Errorf("version is required for action %q", action) + } + if kind == "" { + return fmt.Errorf("kind is required for action %q", action) + } + } + if action == "create" { + if name == "" { + return fmt.Errorf("name is required for action %q", action) + } + if jsonData == "" { + return fmt.Errorf("json_data is required for action %q", action) + } + } + if action == "patch" { + if jsonData == "" { + return fmt.Errorf("json_data is required for action %q", action) + } + } + return nil +} diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index c89afcd4..c71c245b 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -24,12 +24,7 @@ func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool { return slices.Concat( initGraph(), initMeshStatus(), - initIstioConfig(), - initIstioObjectDetails(), - initIstioObjectPatch(), - initIstioObjectCreate(), - initIstioObjectDelete(), - initValidations(), + initManageIstioConfig(), initNamespaces(), initServices(), initWorkloads(), diff --git a/pkg/toolsets/kiali/validations.go b/pkg/toolsets/kiali/validations.go deleted file mode 100644 index 6201da9a..00000000 --- a/pkg/toolsets/kiali/validations.go +++ /dev/null @@ -1,86 +0,0 @@ -package kiali - -import ( - "fmt" - "strings" - - "github.com/google/jsonschema-go/jsonschema" - "k8s.io/utils/ptr" - - "github.com/containers/kubernetes-mcp-server/pkg/api" -) - -func initValidations() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_validations_list", - Description: "List all the validations in the current cluster from all namespaces", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Optional single namespace to retrieve validations from (alternative to namespaces)", - }, - "namespaces": { - Type: "string", - Description: "Optional comma-separated list of namespaces to retrieve validations from", - }, - }, - Required: []string{}, - }, - Annotations: api.ToolAnnotations{ - Title: "Validations: List", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(false), - OpenWorldHint: ptr.To(true), - }, - }, Handler: validationsList, - }) - return ret -} - -func validationsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Parse arguments: allow either `namespace` or `namespaces` (comma-separated string) - namespaces := make([]string, 0) - if v, ok := params.GetArguments()["namespace"].(string); ok { - v = strings.TrimSpace(v) - if v != "" { - namespaces = append(namespaces, v) - } - } - if v, ok := params.GetArguments()["namespaces"].(string); ok { - for _, ns := range strings.Split(v, ",") { - ns = strings.TrimSpace(ns) - if ns != "" { - namespaces = append(namespaces, ns) - } - } - } - // Deduplicate namespaces if both provided - if len(namespaces) > 1 { - seen := map[string]struct{}{} - unique := make([]string, 0, len(namespaces)) - for _, ns := range namespaces { - key := strings.TrimSpace(ns) - if key == "" { - continue - } - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - unique = append(unique, key) - } - namespaces = unique - } - - k := params.NewKiali() - content, err := k.ValidationsList(params.Context, namespaces) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to list validations: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -}