Skip to content

Commit 0fade7e

Browse files
committed
feat: discover istio ns + istioctl analyze
1 parent 6ea0c30 commit 0fade7e

File tree

4 files changed

+133
-55
lines changed

4 files changed

+133
-55
lines changed

PROXY_CONFIG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ The Istio MCP Server supports these proxy configuration tools:
1717
- **get-proxy-bootstrap**: Get Envoy bootstrap configuration from a pod
1818
- **get-proxy-config-dump**: Get full Envoy configuration dump from a pod
1919
- **get-proxy-status**: Get proxy status information for all pods or a specific pod
20+
- **get-istio-analyze**: Analyze Istio configuration and report potential issues
2021

2122
## Implementation Details
2223

23-
- Uses `istioctl proxy-config` commands under the hood
24+
- Uses `istioctl proxy-config` commands under the hood (and `istioctl analyze` for configuration analysis)
2425
- Requires `istioctl` to be installed on the system
2526
- Returns JSON formatted output for easy parsing
2627
- Includes proper error handling and timeouts
@@ -30,7 +31,7 @@ The Istio MCP Server supports these proxy configuration tools:
3031

3132
Each tool requires:
3233
- `namespace` (optional, defaults to 'default')
33-
- `pod` (required for most tools, except `get-proxy-status`)
34+
- `pod` (required for most tools, except `get-proxy-status` and `get-istio-analyze`)
3435

3536
### Examples
3637

@@ -49,6 +50,12 @@ get-proxy-status --namespace default
4950

5051
# Get proxy status for a specific pod
5152
get-proxy-status --namespace default --pod my-app-pod
53+
54+
# Analyze Istio configuration for a specific namespace
55+
get-istio-analyze --namespace default
56+
57+
# Analyze Istio configuration for the entire cluster
58+
get-istio-analyze
5259
```
5360

5461
## Prerequisites

pkg/istio/istio.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package istio
33
import (
44
"context"
55
"fmt"
6+
"sort"
67

78
"github.com/fsnotify/fsnotify"
89
"istio.io/client-go/pkg/clientset/versioned"
@@ -443,6 +444,83 @@ func (i *Istio) CheckExternalDependencyAvailability(ctx context.Context, service
443444
return result, nil
444445
}
445446

447+
// DiscoverNamespacesWithSidecars finds namespaces that have pods with Istio sidecars
448+
// and returns them sorted by the number of sidecars (most injected first)
449+
func (i *Istio) DiscoverNamespacesWithSidecars(ctx context.Context) (string, error) {
450+
namespacesWithSidecars := make(map[string]int)
451+
452+
// Get running pods only (server-side filtering)
453+
pods, err := i.kubeClient.CoreV1().Pods("").List(ctx, metav1.ListOptions{
454+
FieldSelector: "status.phase=Running",
455+
})
456+
if err != nil {
457+
return "", fmt.Errorf("failed to list running pods for Istio sidecar discovery: %w", err)
458+
}
459+
460+
// Count sidecars per namespace
461+
for _, pod := range pods.Items {
462+
// Skip pods that are not running or have no containers
463+
if pod.Status.Phase != "Running" || len(pod.Spec.Containers) == 0 {
464+
continue
465+
}
466+
467+
// Check if pod has istio-proxy sidecar
468+
for _, container := range pod.Spec.Containers {
469+
if container.Name == "istio-proxy" {
470+
namespacesWithSidecars[pod.Namespace]++
471+
break
472+
}
473+
}
474+
}
475+
476+
if len(namespacesWithSidecars) == 0 {
477+
return "No namespaces with Istio sidecars found", nil
478+
}
479+
480+
// Create a slice of namespace counts for sorting
481+
type namespaceCount struct {
482+
namespace string
483+
count int
484+
}
485+
486+
var namespaceCounts []namespaceCount
487+
for ns, count := range namespacesWithSidecars {
488+
namespaceCounts = append(namespaceCounts, namespaceCount{namespace: ns, count: count})
489+
}
490+
491+
// Sort by count (descending) and then by namespace name (ascending)
492+
sort.Slice(namespaceCounts, func(i, j int) bool {
493+
if namespaceCounts[i].count != namespaceCounts[j].count {
494+
return namespaceCounts[i].count > namespaceCounts[j].count
495+
}
496+
return namespaceCounts[i].namespace < namespaceCounts[j].namespace
497+
})
498+
499+
// Build result string
500+
result := fmt.Sprintf("Found %d namespaces with Istio sidecars:\n\n", len(namespaceCounts))
501+
result += "Rank | Namespace | Sidecar Count | Recommendation\n"
502+
result += "-----|-----------|---------------|----------------\n"
503+
504+
for rank, nc := range namespaceCounts {
505+
var recommendation string
506+
if rank == 0 {
507+
recommendation = "BEST - Most Istio-injected workloads"
508+
} else if rank < 3 { // 3 is arbitrary, adjust as needed
509+
recommendation = "Good - High Istio adoption"
510+
} else if rank < 5 {
511+
recommendation = "Moderate - Some Istio usage"
512+
} else {
513+
recommendation = "Low - Minimal Istio usage"
514+
}
515+
516+
result += fmt.Sprintf("%4d | %-9s | %13d | %s\n", rank+1, nc.namespace, nc.count, recommendation)
517+
}
518+
519+
result += "\n💡 **Recommendation**: Start with the top-ranked namespace for Istio operations as it likely contains the most Istio configuration and traffic."
520+
521+
return result, nil
522+
}
523+
446524
func (i *Istio) WatchKubeConfig(onKubeConfigChange func() error) {
447525
if i.clientCmdConfig == nil {
448526
klog.V(1).Info("No client config available for kubeconfig watching")

pkg/istio/proxy_config.go

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package istio
33
import (
44
"context"
55
"fmt"
6-
"io"
76
"net/http"
87
"os/exec"
98
"strings"
@@ -69,6 +68,14 @@ func (p *ProxyConfigClient) GetProxyStatusForPod(ctx context.Context, namespace,
6968
return p.execIstioctl(ctx, "proxy-status", fmt.Sprintf("%s.%s", podName, namespace))
7069
}
7170

71+
// GetAnalyze performs Istio configuration analysis and reports potential issues
72+
func (p *ProxyConfigClient) GetAnalyze(ctx context.Context, namespace string) (string, error) {
73+
if namespace != "" {
74+
return p.execIstioctl(ctx, "analyze", "-n", namespace)
75+
}
76+
return p.execIstioctl(ctx, "analyze")
77+
}
78+
7279
// execIstioctl executes istioctl commands with proper error handling and timeout
7380
func (p *ProxyConfigClient) execIstioctl(ctx context.Context, args ...string) (string, error) {
7481
// Create context with timeout
@@ -106,58 +113,6 @@ func NewEnvoyAdminClient() *EnvoyAdminClient {
106113
}
107114
}
108115

109-
// GetConfigDumpDirect retrieves configuration dump directly from Envoy admin API
110-
func (e *EnvoyAdminClient) GetConfigDumpDirect(ctx context.Context, podIP string) (string, error) {
111-
return e.getEnvoyEndpoint(ctx, podIP, "config_dump")
112-
}
113-
114-
// GetClustersDirect retrieves clusters directly from Envoy admin API
115-
func (e *EnvoyAdminClient) GetClustersDirect(ctx context.Context, podIP string) (string, error) {
116-
return e.getEnvoyEndpoint(ctx, podIP, "clusters")
117-
}
118-
119-
// GetListenersDirect retrieves listeners directly from Envoy admin API
120-
func (e *EnvoyAdminClient) GetListenersDirect(ctx context.Context, podIP string) (string, error) {
121-
return e.getEnvoyEndpoint(ctx, podIP, "listeners")
122-
}
123-
124-
// GetStatsDirect retrieves stats directly from Envoy admin API
125-
func (e *EnvoyAdminClient) GetStatsDirect(ctx context.Context, podIP string) (string, error) {
126-
return e.getEnvoyEndpoint(ctx, podIP, "stats")
127-
}
128-
129-
// GetServerInfoDirect retrieves server info directly from Envoy admin API
130-
func (e *EnvoyAdminClient) GetServerInfoDirect(ctx context.Context, podIP string) (string, error) {
131-
return e.getEnvoyEndpoint(ctx, podIP, "server_info")
132-
}
133-
134-
// getEnvoyEndpoint makes HTTP request to Envoy admin endpoint
135-
func (e *EnvoyAdminClient) getEnvoyEndpoint(ctx context.Context, podIP, endpoint string) (string, error) {
136-
url := fmt.Sprintf("http://%s:15000/%s", podIP, endpoint)
137-
138-
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
139-
if err != nil {
140-
return "", fmt.Errorf("failed to create request: %w", err)
141-
}
142-
143-
resp, err := e.httpClient.Do(req)
144-
if err != nil {
145-
return "", fmt.Errorf("failed to make request to %s: %w", url, err)
146-
}
147-
defer resp.Body.Close()
148-
149-
if resp.StatusCode != http.StatusOK {
150-
return "", fmt.Errorf("unexpected status code %d from %s", resp.StatusCode, url)
151-
}
152-
153-
body, err := io.ReadAll(resp.Body)
154-
if err != nil {
155-
return "", fmt.Errorf("failed to read response body: %w", err)
156-
}
157-
158-
return string(body), nil
159-
}
160-
161116
// ProxyConfigSummary provides a summary of all proxy configurations in a namespace
162117
func (p *ProxyConfigClient) ProxyConfigSummary(ctx context.Context, namespace string) (string, error) {
163118
// Get proxy status first to identify pods

pkg/mcp/profile.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ func (s *Server) initSecurityTools() []server.ServerTool {
144144
// initConfigurationTools initializes configuration-related Istio tools
145145
func (s *Server) initConfigurationTools() []server.ServerTool {
146146
return []server.ServerTool{
147+
{
148+
Tool: mcp.NewTool("discover-istio-namespaces",
149+
mcp.WithDescription("Discover namespaces that have pods with Istio sidecars and rank them by injection density. This tool helps identify the most probable best namespace for Istio operations by analyzing which namespaces have the most Istio-injected workloads. Use this to prioritize which namespaces to investigate first for Istio configuration and traffic analysis."),
150+
mcp.WithTitleAnnotation("Istio: Namespace Discovery"),
151+
mcp.WithReadOnlyHintAnnotation(true),
152+
mcp.WithDestructiveHintAnnotation(false),
153+
),
154+
Handler: s.discoverIstioNamespaces,
155+
},
147156
{
148157
Tool: mcp.NewTool("get-envoy-filters",
149158
mcp.WithDescription("Get Istio Envoy Filters from any namespace. Envoy Filters allow custom configuration of Envoy proxy behavior, including custom filters, listeners, and clusters. Use this to inspect advanced Istio service mesh configurations."),
@@ -317,6 +326,18 @@ func (s *Server) initProxyConfigTools() []server.ServerTool {
317326
),
318327
Handler: s.getProxyStatus,
319328
},
329+
{
330+
Tool: mcp.NewTool("get-istio-analyze",
331+
mcp.WithDescription("Analyze Istio configuration and report potential issues, misconfigurations, and best practice violations. This tool runs 'istioctl analyze' to provide comprehensive analysis of your Istio service mesh configuration."),
332+
mcp.WithString("namespace",
333+
mcp.Description("Namespace to analyze (optional). If specified, analyzes only the specified namespace. If not provided, analyzes the entire cluster."),
334+
),
335+
mcp.WithTitleAnnotation("Istio: Configuration Analysis"),
336+
mcp.WithReadOnlyHintAnnotation(true),
337+
mcp.WithDestructiveHintAnnotation(false),
338+
),
339+
Handler: s.getIstioAnalyze,
340+
},
320341
}
321342
}
322343

@@ -431,6 +452,12 @@ func (s *Server) checkExternalDependencyAvailability(ctx context.Context, ctr mc
431452
return NewTextResult(content, err), nil
432453
}
433454

455+
// Handler method for Istio namespace discovery
456+
func (s *Server) discoverIstioNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
457+
content, err := s.i.DiscoverNamespacesWithSidecars(ctx)
458+
return NewTextResult(content, err), nil
459+
}
460+
434461
// Handler methods for proxy configuration tools
435462
func (s *Server) getProxyClusters(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
436463
namespace := "default"
@@ -552,6 +579,17 @@ func (s *Server) getProxyStatus(ctx context.Context, ctr mcp.CallToolRequest) (*
552579
return NewTextResult(content, err), nil
553580
}
554581

582+
// getIstioAnalyze performs Istio configuration analysis
583+
func (s *Server) getIstioAnalyze(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
584+
namespace := ""
585+
if ns := ctr.GetArguments()["namespace"]; ns != nil {
586+
namespace = ns.(string)
587+
}
588+
589+
content, err := s.i.ProxyConfig.GetAnalyze(ctx, namespace)
590+
return NewTextResult(content, err), nil
591+
}
592+
555593
func init() {
556594
ProfileNames = make([]string, 0)
557595
for _, profile := range Profiles {

0 commit comments

Comments
 (0)