From bbc114403aa3aff9b1e6a38d5c0d07e69d709c21 Mon Sep 17 00:00:00 2001 From: Alberto Gutierrez Date: Fri, 21 Nov 2025 17:14:49 +0100 Subject: [PATCH 1/2] feat(kiali): Consolidate tools Signed-off-by: Alberto Gutierrez --- README.md | 125 ++------ pkg/kiali/endpoints.go | 1 - pkg/kiali/get_mesh_graph.go | 133 ++++++++ pkg/kiali/graph.go | 15 +- pkg/kiali/health.go | 15 +- pkg/kiali/istio.go | 193 +++++------- .../kiali/{graph.go => get_mesh_graph.go} | 31 +- pkg/toolsets/kiali/get_metrics.go | 164 ++++++++++ pkg/toolsets/kiali/get_resource_details.go | 119 ++++++++ pkg/toolsets/kiali/get_traces.go | 157 ++++++++++ pkg/toolsets/kiali/health.go | 80 ----- pkg/toolsets/kiali/istio_config.go | 288 ------------------ pkg/toolsets/kiali/manage_istio_config.go | 117 +++++++ pkg/toolsets/kiali/mesh.go | 42 --- pkg/toolsets/kiali/namespaces.go | 40 --- pkg/toolsets/kiali/services.go | 209 ------------- pkg/toolsets/kiali/toolset.go | 18 +- pkg/toolsets/kiali/traces.go | 285 ----------------- pkg/toolsets/kiali/validations.go | 86 ------ pkg/toolsets/kiali/workloads.go | 209 ------------- 20 files changed, 840 insertions(+), 1487 deletions(-) create mode 100644 pkg/kiali/get_mesh_graph.go rename pkg/toolsets/kiali/{graph.go => get_mesh_graph.go} (62%) create mode 100644 pkg/toolsets/kiali/get_metrics.go create mode 100644 pkg/toolsets/kiali/get_resource_details.go create mode 100644 pkg/toolsets/kiali/get_traces.go delete mode 100644 pkg/toolsets/kiali/health.go delete mode 100644 pkg/toolsets/kiali/istio_config.go create mode 100644 pkg/toolsets/kiali/manage_istio_config.go delete mode 100644 pkg/toolsets/kiali/mesh.go delete mode 100644 pkg/toolsets/kiali/namespaces.go delete mode 100644 pkg/toolsets/kiali/services.go delete mode 100644 pkg/toolsets/kiali/traces.go delete mode 100644 pkg/toolsets/kiali/validations.go delete mode 100644 pkg/toolsets/kiali/workloads.go diff --git a/README.md b/README.md index ffd12a25..4a623032 100644 --- a/README.md +++ b/README.md @@ -348,92 +348,38 @@ In case multi-cluster support is enabled (default) and you have access to multip kiali -- **kiali_graph** - Check the status of my mesh by querying Kiali graph +- **kiali_get_mesh_graph** - Returns the topology of a specific namespaces, health, status of the mesh and namespaces. Use this for high-level overviews + - `graphType` (`string`) - Type of graph to return: 'versionedApp', 'app', 'service', 'workload', 'mesh'. Default: 'versionedApp' - `namespace` (`string`) - Optional single namespace to include in the graph (alternative to namespaces) - `namespaces` (`string`) - Optional comma-separated list of namespaces to include in the graph - -- **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_namespaces** - Get all namespaces in the mesh that the user has access to - -- **kiali_services_list** - Get all services in the mesh across specified namespaces with health and Istio resource information + - `rateInterval` (`string`) - Rate interval for fetching (e.g., '10m', '5m', '1h'). Default: '60s' + +- **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_get_resource_details** - Gets lists or detailed info for Kubernetes resources (services, workloads) within the mesh - `namespaces` (`string`) - Comma-separated list of namespaces to get services from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, will list services from all accessible namespaces + - `resource_name` (`string`) - Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all). + - `resource_type` (`string`) - Type of resource to get details for (service, workload) -- **kiali_service_details** - Get detailed information for a specific service in a namespace, including validation, health status, and configuration - - `namespace` (`string`) **(required)** - Namespace containing the service - - `service` (`string`) **(required)** - Name of the service to get details for - -- **kiali_service_metrics** - Get metrics for a specific service in a namespace. Supports filtering by time range, direction (inbound/outbound), reporter, and other query parameters - - `byLabels` (`string`) - Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional - - `direction` (`string`) - Traffic direction: 'inbound' or 'outbound'. Optional, defaults to 'outbound' - - `duration` (`string`) - Duration of the query period in seconds (e.g., '1800' for 30 minutes). Optional, defaults to 1800 seconds - - `namespace` (`string`) **(required)** - Namespace containing the service - - `quantiles` (`string`) - Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional - - `rateInterval` (`string`) - Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '1m' - - `reporter` (`string`) - Metrics reporter: 'source', 'destination', or 'both'. Optional, defaults to 'source' - - `requestProtocol` (`string`) - Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional - - `service` (`string`) **(required)** - Name of the service to get metrics for - - `step` (`string`) - Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds - -- **kiali_workloads_list** - Get all workloads in the mesh across specified namespaces with health and Istio resource information - - `namespaces` (`string`) - Comma-separated list of namespaces to get workloads from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, will list workloads from all accessible namespaces - -- **kiali_workload_details** - Get detailed information for a specific workload in a namespace, including validation, health status, and configuration - - `namespace` (`string`) **(required)** - Namespace containing the workload - - `workload` (`string`) **(required)** - Name of the workload to get details for - -- **kiali_workload_metrics** - Get metrics for a specific workload in a namespace. Supports filtering by time range, direction (inbound/outbound), reporter, and other query parameters +- **kiali_get_metrics** - Gets lists or detailed info for Kubernetes resources (services, workloads) within the mesh - `byLabels` (`string`) - Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional - `direction` (`string`) - Traffic direction: 'inbound' or 'outbound'. Optional, defaults to 'outbound' - - `duration` (`string`) - Duration of the query period in seconds (e.g., '1800' for 30 minutes). Optional, defaults to 1800 seconds - - `namespace` (`string`) **(required)** - Namespace containing the workload + - `duration` (`string`) - Time range to get metrics for (optional string - if provided, gets metrics; if empty, get default 1800s). + - `namespace` (`string`) **(required)** - Namespace to get resources from - `quantiles` (`string`) - Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional - `rateInterval` (`string`) - Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '1m' - `reporter` (`string`) - Metrics reporter: 'source', 'destination', or 'both'. Optional, defaults to 'source' - `requestProtocol` (`string`) - Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional + - `resource_name` (`string`) **(required)** - Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all). + - `resource_type` (`string`) **(required)** - Type of resource to get details for (service, workload) - `step` (`string`) - Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds - - `workload` (`string`) **(required)** - Name of the workload to get metrics for - -- **kiali_health** - Get health status for apps, workloads, and services across specified namespaces in the mesh. Returns health information including error rates and status for the requested resource type - - `namespaces` (`string`) - Comma-separated list of namespaces to get health from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, returns health for all accessible namespaces - - `queryTime` (`string`) - Unix timestamp (in seconds) for the prometheus query. If not provided, uses current time. Optional - - `rateInterval` (`string`) - Rate interval for fetching error rate (e.g., '10m', '5m', '1h'). Default: '10m' - - `type` (`string`) - Type of health to retrieve: 'app', 'service', or 'workload'. Default: 'app' - **workload_logs** - Get logs for a specific workload's pods in a namespace. Only requires namespace and workload name - automatically discovers pods and containers. Optionally filter by container name, time range, and other parameters. Container is auto-detected if not specified. - `container` (`string`) - Optional container name to filter logs. If not provided, automatically detects and uses the main application container (excludes istio-proxy and istio-init) @@ -442,36 +388,17 @@ In case multi-cluster support is enabled (default) and you have access to multip - `tail` (`integer`) - Number of lines to retrieve from the end of logs (default: 100) - `workload` (`string`) **(required)** - Name of the workload to get logs for -- **kiali_app_traces** - Get distributed tracing data for a specific app in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis. - - `app` (`string`) **(required)** - Name of the app to get traces for +- **kiali_get_traces** - Gets traces for a specific resource (app, service, workload) in a namespace - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional) - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional) - `limit` (`integer`) - Maximum number of traces to return (default: 100) - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional) - - `namespace` (`string`) **(required)** - Namespace containing the app + - `namespace` (`string`) **(required)** - Namespace to get resources from + - `resource_name` (`string`) **(required)** - Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all). + - `resource_type` (`string`) **(required)** - Type of resource to get metrics for (app, service, workload) - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional) - `tags` (`string`) - JSON string of tags to filter traces (optional) -- **kiali_service_traces** - Get distributed tracing data for a specific service in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis. - - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional) - - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional) - - `limit` (`integer`) - Maximum number of traces to return (default: 100) - - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional) - - `namespace` (`string`) **(required)** - Namespace containing the service - - `service` (`string`) **(required)** - Name of the service to get traces for - - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional) - - `tags` (`string`) - JSON string of tags to filter traces (optional) - -- **kiali_workload_traces** - Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis. - - `clusterName` (`string`) - Cluster name for multi-cluster environments (optional) - - `endMicros` (`string`) - End time for traces in microseconds since epoch (optional) - - `limit` (`integer`) - Maximum number of traces to return (default: 100) - - `minDuration` (`integer`) - Minimum trace duration in microseconds (optional) - - `namespace` (`string`) **(required)** - Namespace containing the workload - - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional) - - `tags` (`string`) - JSON string of tags to filter traces (optional) - - `workload` (`string`) **(required)** - Name of the workload to get traces for - @@ -488,4 +415,4 @@ Compile the project and run the Kubernetes MCP server with [mcp-inspector](https make build # Run the Kubernetes MCP server with mcp-inspector npx @modelcontextprotocol/inspector@latest $(pwd)/kubernetes-mcp-server -``` +``` \ No newline at end of file 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/get_mesh_graph.go b/pkg/kiali/get_mesh_graph.go new file mode 100644 index 00000000..a663ccff --- /dev/null +++ b/pkg/kiali/get_mesh_graph.go @@ -0,0 +1,133 @@ +package kiali + +import ( + "context" + "encoding/json" + "strings" + "sync" +) + +type GetMeshGraphResponse struct { + Graph json.RawMessage `json:"graph,omitempty"` + Health json.RawMessage `json:"health,omitempty"` + MeshStatus json.RawMessage `json:"mesh_status,omitempty"` + Namespaces json.RawMessage `json:"namespaces,omitempty"` + Errors map[string]string `json:"errors,omitempty"` +} + +// GetMeshGraph fetches multiple Kiali endpoints in parallel and returns a combined response. +// Each field in the response corresponds to one API call result. +// - graph: /api/namespaces/graph (optionally filtered by namespaces) +// - health: /api/clusters/health (optionally filtered by namespaces and queryParams) +// - status(mesh):/api/mesh/graph +// - namespaces: /api/namespaces +func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryParams map[string]string) (string, error) { + cleaned := make([]string, 0, len(namespaces)) + for _, ns := range namespaces { + ns = strings.TrimSpace(ns) + if ns != "" { + cleaned = append(cleaned, ns) + } + } + + resp := GetMeshGraphResponse{ + Errors: make(map[string]string), + } + + var wg sync.WaitGroup + wg.Add(4) + + // Graph + go func() { + defer wg.Done() + data, err := k.getGraph(ctx, cleaned, queryParams) + if err != nil { + resp.Errors["graph"] = err.Error() + return + } + resp.Graph = data + }() + + // Health + go func() { + defer wg.Done() + data, err := k.getHealth(ctx, cleaned, queryParams) + if err != nil { + resp.Errors["health"] = err.Error() + return + } + resp.Health = data + }() + + // Mesh status + go func() { + defer wg.Done() + data, err := k.getMeshStatus(ctx) + if err != nil { + resp.Errors["mesh_status"] = err.Error() + return + } + resp.MeshStatus = data + }() + + // Namespaces + go func() { + defer wg.Done() + data, err := k.getNamespaces(ctx) + if err != nil { + resp.Errors["namespaces"] = err.Error() + return + } + resp.Namespaces = data + }() + + wg.Wait() + + // If no errors occurred, omit the errors map in the final JSON + if len(resp.Errors) == 0 { + resp.Errors = nil + } + + encoded, err := json.Marshal(resp) + if err != nil { + return "", err + } + return string(encoded), nil +} + +// getGraph wraps the Graph call and returns raw JSON. +func (k *Kiali) getGraph(ctx context.Context, namespaces []string, queryParams map[string]string) (json.RawMessage, error) { + out, err := k.Graph(ctx, namespaces, queryParams) + if err != nil { + return nil, err + } + return json.RawMessage([]byte(out)), nil +} + +// getHealth wraps the Health call and returns raw JSON. +func (k *Kiali) getHealth(ctx context.Context, namespaces []string, queryParams map[string]string) (json.RawMessage, error) { + nsParam := strings.Join(namespaces, ",") + out, err := k.Health(ctx, nsParam, queryParams) + if err != nil { + return nil, err + } + return json.RawMessage([]byte(out)), nil +} + +// getMeshStatus wraps the MeshStatus call and returns raw JSON. +func (k *Kiali) getMeshStatus(ctx context.Context) (json.RawMessage, error) { + out, err := k.MeshStatus(ctx) + if err != nil { + return nil, err + } + return json.RawMessage([]byte(out)), nil +} + +// getNamespaces wraps the ListNamespaces call and returns raw JSON. +func (k *Kiali) getNamespaces(ctx context.Context) (json.RawMessage, error) { + out, err := k.ListNamespaces(ctx) + if err != nil { + return nil, err + } + return json.RawMessage([]byte(out)), nil +} diff --git a/pkg/kiali/graph.go b/pkg/kiali/graph.go index be3ac3c4..c699df2d 100644 --- a/pkg/kiali/graph.go +++ b/pkg/kiali/graph.go @@ -10,15 +10,24 @@ import ( // Graph calls the Kiali graph API using the provided Authorization header value. // `namespaces` may contain zero, one or many namespaces. If empty, the API may return an empty graph // or the server default, depending on Kiali configuration. -func (k *Kiali) Graph(ctx context.Context, namespaces []string) (string, error) { +func (k *Kiali) Graph(ctx context.Context, namespaces []string, queryParams map[string]string) (string, error) { u, err := url.Parse(GraphEndpoint) if err != nil { return "", err } q := u.Query() // Static graph parameters per requirements - q.Set("duration", "60s") - q.Set("graphType", "versionedApp") + // Defaults with optional overrides via queryParams + duration := "60s" + graphType := "versionedApp" + if v, ok := queryParams["rateInterval"]; ok && strings.TrimSpace(v) != "" { + duration = strings.TrimSpace(v) + } + if v, ok := queryParams["graphType"]; ok && strings.TrimSpace(v) != "" { + graphType = strings.TrimSpace(v) + } + q.Set("duration", duration) + q.Set("graphType", graphType) q.Set("includeIdleEdges", "false") q.Set("injectServiceNodes", "true") q.Set("boxBy", "cluster,namespace,app") diff --git a/pkg/kiali/health.go b/pkg/kiali/health.go index ff9d6226..15aba2e3 100644 --- a/pkg/kiali/health.go +++ b/pkg/kiali/health.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/url" + "strings" ) // Health returns health status for apps, workloads, and services across namespaces. @@ -11,7 +12,7 @@ import ( // - namespaces: comma-separated list of namespaces (optional, if empty returns health for all accessible namespaces) // - queryParams: optional query parameters map for filtering health data (e.g., "type", "rateInterval", "queryTime") // - type: health type - "app", "service", or "workload" (default: "app") -// - rateInterval: rate interval for fetching error rate (default: "10m") +// - rateInterval: rate interval for fetching error rate (default: "1m") // - queryTime: Unix timestamp for the prometheus query (optional) func (k *Kiali) Health(ctx context.Context, namespaces string, queryParams map[string]string) (string, error) { // Build query parameters @@ -33,6 +34,18 @@ func (k *Kiali) Health(ctx context.Context, namespaces string, queryParams map[s } } + // Ensure health "type" aligns with graphType (versionedApp -> app) + healthType := "app" + if gt, ok := queryParams["graphType"]; ok && strings.TrimSpace(gt) != "" { + v := strings.TrimSpace(gt) + if strings.EqualFold(v, "versionedApp") { + healthType = "app" + } else { + healthType = v + } + } + q.Set("type", healthType) + u.RawQuery = q.Encode() endpoint := u.String() 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/graph.go b/pkg/toolsets/kiali/get_mesh_graph.go similarity index 62% rename from pkg/toolsets/kiali/graph.go rename to pkg/toolsets/kiali/get_mesh_graph.go index a0a50f92..cc1ac5d1 100644 --- a/pkg/toolsets/kiali/graph.go +++ b/pkg/toolsets/kiali/get_mesh_graph.go @@ -10,12 +10,12 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/api" ) -func initGraph() []api.ServerTool { +func initGetMeshGraph() []api.ServerTool { ret := make([]api.ServerTool, 0) ret = append(ret, api.ServerTool{ Tool: api.Tool{ - Name: "kiali_graph", - Description: "Check the status of my mesh by querying Kiali graph", + Name: "kiali_get_mesh_graph", + Description: "Returns the topology of a specific namespaces, health, status of the mesh and namespaces. Use this for high-level overviews", InputSchema: &jsonschema.Schema{ Type: "object", Properties: map[string]*jsonschema.Schema{ @@ -27,22 +27,30 @@ func initGraph() []api.ServerTool { Type: "string", Description: "Optional comma-separated list of namespaces to include in the graph", }, + "rateInterval": { + Type: "string", + Description: "Rate interval for fetching (e.g., '10m', '5m', '1h'). Default: '60s'", + }, + "graphType": { + Type: "string", + Description: "Type of graph to return: 'versionedApp', 'app', 'service', 'workload', 'mesh'. Default: 'versionedApp'", + }, }, Required: []string{}, }, Annotations: api.ToolAnnotations{ - Title: "Graph: Mesh status", + Title: "Topology: Mesh, Graph, Health, and Status", ReadOnlyHint: ptr.To(true), DestructiveHint: ptr.To(false), IdempotentHint: ptr.To(false), OpenWorldHint: ptr.To(true), }, - }, Handler: graphHandler, + }, Handler: getMeshGraphHandler, }) return ret } -func graphHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { +func getMeshGraphHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { // Parse arguments: allow either `namespace` or `namespaces` (comma-separated string) namespaces := make([]string, 0) @@ -77,8 +85,17 @@ func graphHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } namespaces = unique } + + // Extract optional query parameters + queryParams := make(map[string]string) + if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { + queryParams["rateInterval"] = rateInterval + } + if graphType, ok := params.GetArguments()["graph_type"].(string); ok && graphType != "" { + queryParams["graphType"] = graphType + } k := params.NewKiali() - content, err := k.Graph(params.Context, namespaces) + content, err := k.GetMeshGraph(params.Context, namespaces, queryParams) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to retrieve mesh graph: %v", err)), nil } diff --git a/pkg/toolsets/kiali/get_metrics.go b/pkg/toolsets/kiali/get_metrics.go new file mode 100644 index 00000000..acedaf65 --- /dev/null +++ b/pkg/toolsets/kiali/get_metrics.go @@ -0,0 +1,164 @@ +package kiali + +import ( + "context" + "fmt" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + kialiclient "github.com/containers/kubernetes-mcp-server/pkg/kiali" +) + +type resourceOperations struct { + singularName string + metricsFunc func(ctx context.Context, k *kialiclient.Kiali, namespace, name string, queryParams map[string]string) (string, error) +} + +var opsMap = map[string]resourceOperations{ + "service": { + singularName: "service", + metricsFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string, queryParams map[string]string) (string, error) { + return k.ServiceMetrics(ctx, ns, name, queryParams) + }, + }, + "workload": { + singularName: "workload", + metricsFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string, queryParams map[string]string) (string, error) { + return k.WorkloadMetrics(ctx, ns, name, queryParams) + }, + }, +} + +func initGetMetrics() []api.ServerTool { + ret := make([]api.ServerTool, 0) + + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "kiali_get_metrics", + Description: "Gets lists or detailed info for Kubernetes resources (services, workloads) within the mesh", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "resource_type": { + Type: "string", + Description: "Type of resource to get details for (service, workload)", + Enum: []any{"service", "workload"}, + }, + "namespace": { + Type: "string", + Description: "Namespace to get resources from", + }, + "resource_name": { + Type: "string", + Description: "Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).", + }, + "duration": { + Type: "string", + Description: "Time range to get metrics for (optional string - if provided, gets metrics; if empty, get default 1800s).", + }, + "step": { + Type: "string", + Description: "Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds", + }, + "rateInterval": { + Type: "string", + Description: "Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '1m'", + }, + "direction": { + Type: "string", + Description: "Traffic direction: 'inbound' or 'outbound'. Optional, defaults to 'outbound'", + }, + "reporter": { + Type: "string", + Description: "Metrics reporter: 'source', 'destination', or 'both'. Optional, defaults to 'source'", + }, + "requestProtocol": { + Type: "string", + Description: "Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional", + }, + "quantiles": { + Type: "string", + Description: "Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional", + }, + "byLabels": { + Type: "string", + Description: "Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional", + }, + }, + Required: []string{"resource_type", "namespace", "resource_name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Get Metrics for a Resource", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: resourceMetricsHandler, + }) + + return ret +} + +func resourceMetricsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + resourceType, _ := params.GetArguments()["resource_type"].(string) + namespace, _ := params.GetArguments()["namespace"].(string) + resourceName, _ := params.GetArguments()["resource_name"].(string) + + resourceType = strings.ToLower(strings.TrimSpace(resourceType)) + namespace = strings.TrimSpace(namespace) + resourceName = strings.TrimSpace(resourceName) + + if resourceType == "" { + return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil + } + if namespace == "" || len(strings.Split(namespace, ",")) != 1 { + return api.NewToolCallResult("", fmt.Errorf("namespace is required")), nil + } + if resourceName == "" { + return api.NewToolCallResult("", fmt.Errorf("resource_name is required")), nil + } + + ops, ok := opsMap[resourceType] + if !ok { + return api.NewToolCallResult("", fmt.Errorf("invalid resource type: %s", resourceType)), nil + } + + queryParams := make(map[string]string) + if duration, ok := params.GetArguments()["duration"].(string); ok && duration != "" { + queryParams["duration"] = duration + } + if step, ok := params.GetArguments()["step"].(string); ok && step != "" { + queryParams["step"] = step + } + if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { + queryParams["rateInterval"] = rateInterval + } + if direction, ok := params.GetArguments()["direction"].(string); ok && direction != "" { + queryParams["direction"] = direction + } + if reporter, ok := params.GetArguments()["reporter"].(string); ok && reporter != "" { + queryParams["reporter"] = reporter + } + if requestProtocol, ok := params.GetArguments()["requestProtocol"].(string); ok && requestProtocol != "" { + queryParams["requestProtocol"] = requestProtocol + } + if quantiles, ok := params.GetArguments()["quantiles"].(string); ok && quantiles != "" { + queryParams["quantiles"] = quantiles + } + if byLabels, ok := params.GetArguments()["byLabels"].(string); ok && byLabels != "" { + queryParams["byLabels"] = byLabels + } + + k := params.NewKiali() + + content, err := ops.metricsFunc(params.Context, k, namespace, resourceName, queryParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get %s metrics: %v", ops.singularName, err)), nil + } + return api.NewToolCallResult(content, nil), nil +} diff --git a/pkg/toolsets/kiali/get_resource_details.go b/pkg/toolsets/kiali/get_resource_details.go new file mode 100644 index 00000000..308b1858 --- /dev/null +++ b/pkg/toolsets/kiali/get_resource_details.go @@ -0,0 +1,119 @@ +package kiali + +import ( + "context" + "fmt" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + kialiclient "github.com/containers/kubernetes-mcp-server/pkg/kiali" +) + +type listDetailsOperations struct { + singularName string + listFunc func(ctx context.Context, k *kialiclient.Kiali, namespaces string) (string, error) + detailsFunc func(ctx context.Context, k *kialiclient.Kiali, namespace, name string) (string, error) +} + +var listDetailsOpsMap = map[string]listDetailsOperations{ + "service": { + singularName: "service", + listFunc: func(ctx context.Context, k *kialiclient.Kiali, nss string) (string, error) { + return k.ServicesList(ctx, nss) + }, + detailsFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string) (string, error) { + return k.ServiceDetails(ctx, ns, name) + }, + }, + "workload": { + singularName: "workload", + listFunc: func(ctx context.Context, k *kialiclient.Kiali, nss string) (string, error) { + return k.WorkloadsList(ctx, nss) + }, + detailsFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string) (string, error) { + return k.WorkloadDetails(ctx, ns, name) + }, + }, +} + +func initGetResourceDetails() []api.ServerTool { + ret := make([]api.ServerTool, 0) + + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "kiali_get_resource_details", + Description: "Gets lists or detailed info for Kubernetes resources (services, workloads) within the mesh", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "resource_type": { + Type: "string", + Description: "Type of resource to get details for (service, workload)", + Enum: []any{"service", "workload"}, + }, + "namespaces": { + Type: "string", + Description: "Comma-separated list of namespaces to get services from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, will list services from all accessible namespaces", + }, + "resource_name": { + Type: "string", + Description: "Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).", + }, + }, + }, + Annotations: api.ToolAnnotations{ + Title: "List or Resource Details", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: resourceDetailsHandler, + }) + + return ret +} + +func resourceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + resourceType, _ := params.GetArguments()["resource_type"].(string) + namespaces, _ := params.GetArguments()["namespaces"].(string) + resourceName, _ := params.GetArguments()["resource_name"].(string) + + resourceType = strings.ToLower(strings.TrimSpace(resourceType)) + namespaces = strings.TrimSpace(namespaces) + resourceName = strings.TrimSpace(resourceName) + + if resourceType == "" { + return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil + } + + k := params.NewKiali() + + ops, ok := listDetailsOpsMap[resourceType] + if !ok { + return api.NewToolCallResult("", fmt.Errorf("invalid resource type: %s", resourceType)), nil + } + + // If a resource name is provided, fetch details. Requires exactly one namespace. + if resourceName != "" { + if count := len(strings.Split(namespaces, ",")); count != 1 { + return api.NewToolCallResult("", fmt.Errorf("exactly one namespace must be provided for %s details", ops.singularName)), nil + } + content, err := ops.detailsFunc(params.Context, k, namespaces, resourceName) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get %s details: %v", ops.singularName, err)), nil + } + return api.NewToolCallResult(content, nil), nil + } + + // Otherwise, list resources (supports multiple namespaces) + content, err := ops.listFunc(params.Context, k, namespaces) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to list %ss: %v", ops.singularName, err)), nil + } + return api.NewToolCallResult(content, nil), nil +} diff --git a/pkg/toolsets/kiali/get_traces.go b/pkg/toolsets/kiali/get_traces.go new file mode 100644 index 00000000..65db5647 --- /dev/null +++ b/pkg/toolsets/kiali/get_traces.go @@ -0,0 +1,157 @@ +package kiali + +import ( + "context" + "fmt" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + kialiclient "github.com/containers/kubernetes-mcp-server/pkg/kiali" +) + +type tracesOperations struct { + singularName string + tracesFunc func(ctx context.Context, k *kialiclient.Kiali, namespace, name string, queryParams map[string]string) (string, error) +} + +var tracesOpsMap = map[string]tracesOperations{ + "app": { + singularName: "app", + tracesFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string, queryParams map[string]string) (string, error) { + return k.AppTraces(ctx, ns, name, queryParams) + }, + }, + "service": { + singularName: "service", + tracesFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string, queryParams map[string]string) (string, error) { + return k.ServiceTraces(ctx, ns, name, queryParams) + }, + }, + "workload": { + singularName: "workload", + tracesFunc: func(ctx context.Context, k *kialiclient.Kiali, ns, name string, queryParams map[string]string) (string, error) { + return k.WorkloadTraces(ctx, ns, name, queryParams) + }, + }, +} + +func initGetTraces() []api.ServerTool { + ret := make([]api.ServerTool, 0) + + ret = append(ret, api.ServerTool{ + Tool: api.Tool{ + Name: "kiali_get_traces", + Description: "Gets traces for a specific resource (app, service, workload) in a namespace", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "resource_type": { + Type: "string", + Description: "Type of resource to get metrics for (app, service, workload)", + Enum: []any{"app", "service", "workload"}, + }, + "namespace": { + Type: "string", + Description: "Namespace to get resources from", + }, + "resource_name": { + Type: "string", + Description: "Name of the resource to get details for (optional string - if provided, gets details; if empty, lists all).", + }, + "startMicros": { + Type: "string", + Description: "Start time for traces in microseconds since epoch (optional)", + }, + "endMicros": { + Type: "string", + Description: "End time for traces in microseconds since epoch (optional)", + }, + "limit": { + Type: "integer", + Description: "Maximum number of traces to return (default: 100)", + Minimum: ptr.To(float64(1)), + }, + "minDuration": { + Type: "integer", + Description: "Minimum trace duration in microseconds (optional)", + Minimum: ptr.To(float64(0)), + }, + "tags": { + Type: "string", + Description: "JSON string of tags to filter traces (optional)", + }, + "clusterName": { + Type: "string", + Description: "Cluster name for multi-cluster environments (optional)", + }, + }, + Required: []string{"resource_type", "namespace", "resource_name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Get Traces for a Resource", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: TracesHandler, + }) + + return ret +} + +func TracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + // Extract parameters + resourceType, _ := params.GetArguments()["resource_type"].(string) + namespace, _ := params.GetArguments()["namespace"].(string) + resourceName, _ := params.GetArguments()["resource_name"].(string) + + resourceType = strings.ToLower(strings.TrimSpace(resourceType)) + namespace = strings.TrimSpace(namespace) + resourceName = strings.TrimSpace(resourceName) + + if resourceType == "" { + return api.NewToolCallResult("", fmt.Errorf("resource_type is required")), nil + } + if namespace == "" || len(strings.Split(namespace, ",")) != 1 { + return api.NewToolCallResult("", fmt.Errorf("namespace is required")), nil + } + if resourceName == "" { + return api.NewToolCallResult("", fmt.Errorf("resource_name is required")), nil + } + + k := params.NewKiali() + + ops, ok := tracesOpsMap[resourceType] + if !ok { + return api.NewToolCallResult("", fmt.Errorf("invalid resource type: %s", resourceType)), nil + } + + queryParams := make(map[string]string) + if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { + queryParams["startMicros"] = startMicros + } + if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { + queryParams["endMicros"] = endMicros + } + if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { + queryParams["limit"] = limit + } + if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { + queryParams["minDuration"] = minDuration + } + if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { + queryParams["tags"] = tags + } + if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { + queryParams["clusterName"] = clusterName + } + content, err := ops.tracesFunc(params.Context, k, namespace, resourceName, queryParams) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get %s traces: %v", ops.singularName, err)), nil + } + return api.NewToolCallResult(content, nil), nil +} diff --git a/pkg/toolsets/kiali/health.go b/pkg/toolsets/kiali/health.go deleted file mode 100644 index dd9f98fb..00000000 --- a/pkg/toolsets/kiali/health.go +++ /dev/null @@ -1,80 +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 initHealth() []api.ServerTool { - ret := make([]api.ServerTool, 0) - - // Cluster health tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_health", - Description: "Get health status for apps, workloads, and services across specified namespaces in the mesh. Returns health information including error rates and status for the requested resource type", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespaces": { - Type: "string", - Description: "Comma-separated list of namespaces to get health from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, returns health for all accessible namespaces", - }, - "type": { - Type: "string", - Description: "Type of health to retrieve: 'app', 'service', or 'workload'. Default: 'app'", - }, - "rateInterval": { - Type: "string", - Description: "Rate interval for fetching error rate (e.g., '10m', '5m', '1h'). Default: '10m'", - }, - "queryTime": { - Type: "string", - Description: "Unix timestamp (in seconds) for the prometheus query. If not provided, uses current time. Optional", - }, - }, - }, - Annotations: api.ToolAnnotations{ - Title: "Health", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: clusterHealthHandler, - }) - - return ret -} - -func clusterHealthHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespaces, _ := params.GetArguments()["namespaces"].(string) - - // Extract optional query parameters - queryParams := make(map[string]string) - if healthType, ok := params.GetArguments()["type"].(string); ok && healthType != "" { - // Validate type parameter - if healthType != "app" && healthType != "service" && healthType != "workload" { - return api.NewToolCallResult("", fmt.Errorf("invalid type parameter: must be one of 'app', 'service', or 'workload'")), nil - } - queryParams["type"] = healthType - } - if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { - queryParams["rateInterval"] = rateInterval - } - if queryTime, ok := params.GetArguments()["queryTime"].(string); ok && queryTime != "" { - queryParams["queryTime"] = queryTime - } - - k := params.NewKiali() - content, err := k.Health(params.Context, namespaces, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get health: %v", err)), nil - } - return api.NewToolCallResult(content, nil), 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/mesh.go b/pkg/toolsets/kiali/mesh.go deleted file mode 100644 index 6e134f8a..00000000 --- a/pkg/toolsets/kiali/mesh.go +++ /dev/null @@ -1,42 +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 initMeshStatus() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_mesh_status", - Description: "Get the status of mesh components including Istio, Kiali, Grafana, Prometheus and their interactions, versions, and health status", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{}, - Required: []string{}, - }, - Annotations: api.ToolAnnotations{ - Title: "Mesh Status: Components Overview", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: meshStatusHandler, - }) - return ret -} - -func meshStatusHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - k := params.NewKiali() - content, err := k.MeshStatus(params.Context) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to retrieve mesh status: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} diff --git a/pkg/toolsets/kiali/namespaces.go b/pkg/toolsets/kiali/namespaces.go deleted file mode 100644 index a380cc82..00000000 --- a/pkg/toolsets/kiali/namespaces.go +++ /dev/null @@ -1,40 +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 initNamespaces() []api.ServerTool { - ret := make([]api.ServerTool, 0) - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_namespaces", - Description: "Get all namespaces in the mesh that the user has access to", - InputSchema: &jsonschema.Schema{ - Type: "object", - }, - Annotations: api.ToolAnnotations{ - Title: "Namespaces: List", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: namespacesHandler, - }) - return ret -} - -func namespacesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - k := params.NewKiali() - content, err := k.ListNamespaces(params.Context) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} diff --git a/pkg/toolsets/kiali/services.go b/pkg/toolsets/kiali/services.go deleted file mode 100644 index 30ff6557..00000000 --- a/pkg/toolsets/kiali/services.go +++ /dev/null @@ -1,209 +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 initServices() []api.ServerTool { - ret := make([]api.ServerTool, 0) - - // Services list tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_services_list", - Description: "Get all services in the mesh across specified namespaces with health and Istio resource information", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespaces": { - Type: "string", - Description: "Comma-separated list of namespaces to get services from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, will list services from all accessible namespaces", - }, - }, - }, - Annotations: api.ToolAnnotations{ - Title: "Services: List", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: servicesListHandler, - }) - - // Service details tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_service_details", - Description: "Get detailed information for a specific service in a namespace, including validation, health status, and configuration", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the service", - }, - "service": { - Type: "string", - Description: "Name of the service to get details for", - }, - }, - Required: []string{"namespace", "service"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Service: Details", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: serviceDetailsHandler, - }) - - // Service metrics tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_service_metrics", - Description: "Get metrics for a specific service in a namespace. Supports filtering by time range, direction (inbound/outbound), reporter, and other query parameters", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the service", - }, - "service": { - Type: "string", - Description: "Name of the service to get metrics for", - }, - "duration": { - Type: "string", - Description: "Duration of the query period in seconds (e.g., '1800' for 30 minutes). Optional, defaults to 1800 seconds", - }, - "step": { - Type: "string", - Description: "Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds", - }, - "rateInterval": { - Type: "string", - Description: "Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '1m'", - }, - "direction": { - Type: "string", - Description: "Traffic direction: 'inbound' or 'outbound'. Optional, defaults to 'outbound'", - }, - "reporter": { - Type: "string", - Description: "Metrics reporter: 'source', 'destination', or 'both'. Optional, defaults to 'source'", - }, - "requestProtocol": { - Type: "string", - Description: "Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional", - }, - "quantiles": { - Type: "string", - Description: "Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional", - }, - "byLabels": { - Type: "string", - Description: "Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional", - }, - }, - Required: []string{"namespace", "service"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Service: Metrics", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: serviceMetricsHandler, - }) - - return ret -} - -func servicesListHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespaces, _ := params.GetArguments()["namespaces"].(string) - - k := params.NewKiali() - content, err := k.ServicesList(params.Context, namespaces) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to list services: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func serviceDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace, _ := params.GetArguments()["namespace"].(string) - service, _ := params.GetArguments()["service"].(string) - - if namespace == "" { - return api.NewToolCallResult("", fmt.Errorf("namespace parameter is required")), nil - } - if service == "" { - return api.NewToolCallResult("", fmt.Errorf("service parameter is required")), nil - } - - k := params.NewKiali() - content, err := k.ServiceDetails(params.Context, namespace, service) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get service details: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func serviceMetricsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - service, _ := params.GetArguments()["service"].(string) - - if namespace == "" { - return api.NewToolCallResult("", fmt.Errorf("namespace parameter is required")), nil - } - if service == "" { - return api.NewToolCallResult("", fmt.Errorf("service parameter is required")), nil - } - - // Extract optional query parameters - queryParams := make(map[string]string) - if duration, ok := params.GetArguments()["duration"].(string); ok && duration != "" { - queryParams["duration"] = duration - } - if step, ok := params.GetArguments()["step"].(string); ok && step != "" { - queryParams["step"] = step - } - if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { - queryParams["rateInterval"] = rateInterval - } - if direction, ok := params.GetArguments()["direction"].(string); ok && direction != "" { - queryParams["direction"] = direction - } - if reporter, ok := params.GetArguments()["reporter"].(string); ok && reporter != "" { - queryParams["reporter"] = reporter - } - if requestProtocol, ok := params.GetArguments()["requestProtocol"].(string); ok && requestProtocol != "" { - queryParams["requestProtocol"] = requestProtocol - } - if quantiles, ok := params.GetArguments()["quantiles"].(string); ok && quantiles != "" { - queryParams["quantiles"] = quantiles - } - if byLabels, ok := params.GetArguments()["byLabels"].(string); ok && byLabels != "" { - queryParams["byLabels"] = byLabels - } - - k := params.NewKiali() - content, err := k.ServiceMetrics(params.Context, namespace, service, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get service metrics: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index c89afcd4..eb8f1ea5 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -22,20 +22,12 @@ func (t *Toolset) GetDescription() string { func (t *Toolset) GetTools(_ internalk8s.Openshift) []api.ServerTool { return slices.Concat( - initGraph(), - initMeshStatus(), - initIstioConfig(), - initIstioObjectDetails(), - initIstioObjectPatch(), - initIstioObjectCreate(), - initIstioObjectDelete(), - initValidations(), - initNamespaces(), - initServices(), - initWorkloads(), - initHealth(), + initGetMeshGraph(), + initManageIstioConfig(), + initGetResourceDetails(), + initGetMetrics(), initLogs(), - initTraces(), + initGetTraces(), ) } diff --git a/pkg/toolsets/kiali/traces.go b/pkg/toolsets/kiali/traces.go deleted file mode 100644 index e9169505..00000000 --- a/pkg/toolsets/kiali/traces.go +++ /dev/null @@ -1,285 +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 initTraces() []api.ServerTool { - ret := make([]api.ServerTool, 0) - - // App traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_app_traces", - Description: "Get distributed tracing data for a specific app in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the app", - }, - "app": { - Type: "string", - Description: "Name of the app to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "app"}, - }, - Annotations: api.ToolAnnotations{ - Title: "App: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: appTracesHandler, - }) - - // Service traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_service_traces", - Description: "Get distributed tracing data for a specific service in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the service", - }, - "service": { - Type: "string", - Description: "Name of the service to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "service"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Service: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: serviceTracesHandler, - }) - - // Workload traces tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_workload_traces", - Description: "Get distributed tracing data for a specific workload in a namespace. Returns trace information including spans, duration, and error details for troubleshooting and performance analysis.", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the workload", - }, - "workload": { - Type: "string", - Description: "Name of the workload to get traces for", - }, - "startMicros": { - Type: "string", - Description: "Start time for traces in microseconds since epoch (optional)", - }, - "endMicros": { - Type: "string", - Description: "End time for traces in microseconds since epoch (optional)", - }, - "limit": { - Type: "integer", - Description: "Maximum number of traces to return (default: 100)", - Minimum: ptr.To(float64(1)), - }, - "minDuration": { - Type: "integer", - Description: "Minimum trace duration in microseconds (optional)", - Minimum: ptr.To(float64(0)), - }, - "tags": { - Type: "string", - Description: "JSON string of tags to filter traces (optional)", - }, - "clusterName": { - Type: "string", - Description: "Cluster name for multi-cluster environments (optional)", - }, - }, - Required: []string{"namespace", "workload"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Workload: Traces", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, - Handler: workloadTracesHandler, - }) - - return ret -} - -func appTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - app := params.GetArguments()["app"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros - } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros - } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit - } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - k := params.NewKiali() - content, err := k.AppTraces(params.Context, namespace, app, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get app traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func serviceTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - service := params.GetArguments()["service"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros - } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros - } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit - } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - - k := params.NewKiali() - content, err := k.ServiceTraces(params.Context, namespace, service, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get service traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func workloadTracesHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace := params.GetArguments()["namespace"].(string) - workload := params.GetArguments()["workload"].(string) - - // Build query parameters from optional arguments - queryParams := make(map[string]string) - if startMicros, ok := params.GetArguments()["startMicros"].(string); ok && startMicros != "" { - queryParams["startMicros"] = startMicros - } - if endMicros, ok := params.GetArguments()["endMicros"].(string); ok && endMicros != "" { - queryParams["endMicros"] = endMicros - } - if limit, ok := params.GetArguments()["limit"].(string); ok && limit != "" { - queryParams["limit"] = limit - } - if minDuration, ok := params.GetArguments()["minDuration"].(string); ok && minDuration != "" { - queryParams["minDuration"] = minDuration - } - if tags, ok := params.GetArguments()["tags"].(string); ok && tags != "" { - queryParams["tags"] = tags - } - if clusterName, ok := params.GetArguments()["clusterName"].(string); ok && clusterName != "" { - queryParams["clusterName"] = clusterName - } - - k := params.NewKiali() - content, err := k.WorkloadTraces(params.Context, namespace, workload, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get workload traces: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} 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 -} diff --git a/pkg/toolsets/kiali/workloads.go b/pkg/toolsets/kiali/workloads.go deleted file mode 100644 index 6d2b30cb..00000000 --- a/pkg/toolsets/kiali/workloads.go +++ /dev/null @@ -1,209 +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 initWorkloads() []api.ServerTool { - ret := make([]api.ServerTool, 0) - - // Workloads list tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_workloads_list", - Description: "Get all workloads in the mesh across specified namespaces with health and Istio resource information", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespaces": { - Type: "string", - Description: "Comma-separated list of namespaces to get workloads from (e.g. 'bookinfo' or 'bookinfo,default'). If not provided, will list workloads from all accessible namespaces", - }, - }, - }, - Annotations: api.ToolAnnotations{ - Title: "Workloads: List", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: workloadsListHandler, - }) - - // Workload details tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_workload_details", - Description: "Get detailed information for a specific workload in a namespace, including validation, health status, and configuration", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the workload", - }, - "workload": { - Type: "string", - Description: "Name of the workload to get details for", - }, - }, - Required: []string{"namespace", "workload"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Workload: Details", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: workloadDetailsHandler, - }) - - // Workload metrics tool - ret = append(ret, api.ServerTool{ - Tool: api.Tool{ - Name: "kiali_workload_metrics", - Description: "Get metrics for a specific workload in a namespace. Supports filtering by time range, direction (inbound/outbound), reporter, and other query parameters", - InputSchema: &jsonschema.Schema{ - Type: "object", - Properties: map[string]*jsonschema.Schema{ - "namespace": { - Type: "string", - Description: "Namespace containing the workload", - }, - "workload": { - Type: "string", - Description: "Name of the workload to get metrics for", - }, - "duration": { - Type: "string", - Description: "Duration of the query period in seconds (e.g., '1800' for 30 minutes). Optional, defaults to 1800 seconds", - }, - "step": { - Type: "string", - Description: "Step between data points in seconds (e.g., '15'). Optional, defaults to 15 seconds", - }, - "rateInterval": { - Type: "string", - Description: "Rate interval for metrics (e.g., '1m', '5m'). Optional, defaults to '1m'", - }, - "direction": { - Type: "string", - Description: "Traffic direction: 'inbound' or 'outbound'. Optional, defaults to 'outbound'", - }, - "reporter": { - Type: "string", - Description: "Metrics reporter: 'source', 'destination', or 'both'. Optional, defaults to 'source'", - }, - "requestProtocol": { - Type: "string", - Description: "Filter by request protocol (e.g., 'http', 'grpc', 'tcp'). Optional", - }, - "quantiles": { - Type: "string", - Description: "Comma-separated list of quantiles for histogram metrics (e.g., '0.5,0.95,0.99'). Optional", - }, - "byLabels": { - Type: "string", - Description: "Comma-separated list of labels to group metrics by (e.g., 'source_workload,destination_service'). Optional", - }, - }, - Required: []string{"namespace", "workload"}, - }, - Annotations: api.ToolAnnotations{ - Title: "Workload: Metrics", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), - IdempotentHint: ptr.To(true), - OpenWorldHint: ptr.To(true), - }, - }, Handler: workloadMetricsHandler, - }) - - return ret -} - -func workloadsListHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespaces, _ := params.GetArguments()["namespaces"].(string) - - k := params.NewKiali() - content, err := k.WorkloadsList(params.Context, namespaces) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to list workloads: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func workloadDetailsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract parameters - namespace, _ := params.GetArguments()["namespace"].(string) - workload, _ := params.GetArguments()["workload"].(string) - - if namespace == "" { - return api.NewToolCallResult("", fmt.Errorf("namespace parameter is required")), nil - } - if workload == "" { - return api.NewToolCallResult("", fmt.Errorf("workload parameter is required")), nil - } - - k := params.NewKiali() - content, err := k.WorkloadDetails(params.Context, namespace, workload) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get workload details: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} - -func workloadMetricsHandler(params api.ToolHandlerParams) (*api.ToolCallResult, error) { - // Extract required parameters - namespace, _ := params.GetArguments()["namespace"].(string) - workload, _ := params.GetArguments()["workload"].(string) - - if namespace == "" { - return api.NewToolCallResult("", fmt.Errorf("namespace parameter is required")), nil - } - if workload == "" { - return api.NewToolCallResult("", fmt.Errorf("workload parameter is required")), nil - } - - // Extract optional query parameters - queryParams := make(map[string]string) - if duration, ok := params.GetArguments()["duration"].(string); ok && duration != "" { - queryParams["duration"] = duration - } - if step, ok := params.GetArguments()["step"].(string); ok && step != "" { - queryParams["step"] = step - } - if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { - queryParams["rateInterval"] = rateInterval - } - if direction, ok := params.GetArguments()["direction"].(string); ok && direction != "" { - queryParams["direction"] = direction - } - if reporter, ok := params.GetArguments()["reporter"].(string); ok && reporter != "" { - queryParams["reporter"] = reporter - } - if requestProtocol, ok := params.GetArguments()["requestProtocol"].(string); ok && requestProtocol != "" { - queryParams["requestProtocol"] = requestProtocol - } - if quantiles, ok := params.GetArguments()["quantiles"].(string); ok && quantiles != "" { - queryParams["quantiles"] = quantiles - } - if byLabels, ok := params.GetArguments()["byLabels"].(string); ok && byLabels != "" { - queryParams["byLabels"] = byLabels - } - - k := params.NewKiali() - content, err := k.WorkloadMetrics(params.Context, namespace, workload, queryParams) - if err != nil { - return api.NewToolCallResult("", fmt.Errorf("failed to get workload metrics: %v", err)), nil - } - return api.NewToolCallResult(content, nil), nil -} From 019f272b647d99d8680c6d54118819eadcc55291 Mon Sep 17 00:00:00 2001 From: Alberto Gutierrez Date: Mon, 24 Nov 2025 21:03:34 +0100 Subject: [PATCH 2/2] Address Marc comments Signed-off-by: Alberto Gutierrez Co-authored-by: Marc Nuri --- pkg/kiali/get_mesh_graph.go | 9 +++ pkg/toolsets/kiali/get_mesh_graph.go | 2 +- pkg/toolsets/kiali/manage_istio_config.go | 67 ++++++++++++++--------- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/pkg/kiali/get_mesh_graph.go b/pkg/kiali/get_mesh_graph.go index a663ccff..8718c815 100644 --- a/pkg/kiali/get_mesh_graph.go +++ b/pkg/kiali/get_mesh_graph.go @@ -34,6 +34,7 @@ func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryPara Errors: make(map[string]string), } + var errorsMu sync.Mutex var wg sync.WaitGroup wg.Add(4) @@ -42,7 +43,9 @@ func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryPara defer wg.Done() data, err := k.getGraph(ctx, cleaned, queryParams) if err != nil { + errorsMu.Lock() resp.Errors["graph"] = err.Error() + errorsMu.Unlock() return } resp.Graph = data @@ -53,7 +56,9 @@ func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryPara defer wg.Done() data, err := k.getHealth(ctx, cleaned, queryParams) if err != nil { + errorsMu.Lock() resp.Errors["health"] = err.Error() + errorsMu.Unlock() return } resp.Health = data @@ -64,7 +69,9 @@ func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryPara defer wg.Done() data, err := k.getMeshStatus(ctx) if err != nil { + errorsMu.Lock() resp.Errors["mesh_status"] = err.Error() + errorsMu.Unlock() return } resp.MeshStatus = data @@ -75,7 +82,9 @@ func (k *Kiali) GetMeshGraph(ctx context.Context, namespaces []string, queryPara defer wg.Done() data, err := k.getNamespaces(ctx) if err != nil { + errorsMu.Lock() resp.Errors["namespaces"] = err.Error() + errorsMu.Unlock() return } resp.Namespaces = data diff --git a/pkg/toolsets/kiali/get_mesh_graph.go b/pkg/toolsets/kiali/get_mesh_graph.go index cc1ac5d1..8981e514 100644 --- a/pkg/toolsets/kiali/get_mesh_graph.go +++ b/pkg/toolsets/kiali/get_mesh_graph.go @@ -91,7 +91,7 @@ func getMeshGraphHandler(params api.ToolHandlerParams) (*api.ToolCallResult, err if rateInterval, ok := params.GetArguments()["rateInterval"].(string); ok && rateInterval != "" { queryParams["rateInterval"] = rateInterval } - if graphType, ok := params.GetArguments()["graph_type"].(string); ok && graphType != "" { + if graphType, ok := params.GetArguments()["graphType"].(string); ok && graphType != "" { queryParams["graphType"] = graphType } k := params.NewKiali() diff --git a/pkg/toolsets/kiali/manage_istio_config.go b/pkg/toolsets/kiali/manage_istio_config.go index f9d165fd..0cbbb014 100644 --- a/pkg/toolsets/kiali/manage_istio_config.go +++ b/pkg/toolsets/kiali/manage_istio_config.go @@ -51,8 +51,8 @@ func initManageIstioConfig() []api.ServerTool { }, Annotations: api.ToolAnnotations{ Title: "Manage Istio Config: List, Get, Create, Patch, Delete", - ReadOnlyHint: ptr.To(true), - DestructiveHint: ptr.To(false), + ReadOnlyHint: ptr.To(false), + DestructiveHint: ptr.To(true), IdempotentHint: ptr.To(true), OpenWorldHint: ptr.To(true), }, @@ -83,35 +83,52 @@ func istioConfigHandler(params api.ToolHandlerParams) (*api.ToolCallResult, erro // 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 +// - If action is "create": json_data are required +// - If action is "patch": name and json_data is required +// - If action is "get": name is required +// - If action is "patch": name 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) + switch action { + case "list", "create", "patch", "get", "delete": + 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 group == "" { - return fmt.Errorf("group is required for action %q", action) + if action == "create" { + if jsonData == "" { + return fmt.Errorf("json_data is required for action %q", action) + } } - if version == "" { - return fmt.Errorf("version is required for action %q", action) + if action == "patch" { + 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 kind == "" { - return fmt.Errorf("kind is required for action %q", action) + if action == "get" { + if name == "" { + return fmt.Errorf("name 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) + if action == "delete" { + if name == "" { + return fmt.Errorf("name is required for action %q", action) + } } + default: + return fmt.Errorf("invalid action %q: must be one of list, create, patch, get, delete", action) } return nil }