@@ -4,9 +4,11 @@ import (
44 "context"
55 "fmt"
66 "sort"
7+ "strings"
78
89 "github.com/fsnotify/fsnotify"
910 "istio.io/client-go/pkg/clientset/versioned"
11+ v1 "k8s.io/api/core/v1"
1012 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1113 "k8s.io/client-go/kubernetes"
1214 "k8s.io/client-go/rest"
@@ -444,6 +446,242 @@ func (i *Istio) CheckExternalDependencyAvailability(ctx context.Context, service
444446 return result , nil
445447}
446448
449+ // GetServices retrieves all Kubernetes services in a namespace
450+ func (i * Istio ) GetServices (ctx context.Context , namespace string ) (string , error ) {
451+ services , err := i .kubeClient .CoreV1 ().Services (namespace ).List (ctx , metav1.ListOptions {})
452+ if err != nil {
453+ return "" , fmt .Errorf ("failed to list services: %w" , err )
454+ }
455+
456+ result := fmt .Sprintf ("Services in namespace '%s':\n \n " , namespace )
457+ result += fmt .Sprintf ("Found %d services:\n \n " , len (services .Items ))
458+
459+ if len (services .Items ) == 0 {
460+ result += "No services found in this namespace.\n "
461+ return result , nil
462+ }
463+
464+ // Group services by type for better organization
465+ var clusterIPServices []string
466+ var nodePortServices []string
467+ var loadBalancerServices []string
468+ var headlessServices []string
469+
470+ for _ , service := range services .Items {
471+ serviceLine := fmt .Sprintf ("%-30s" , service .Name )
472+
473+ // Add service type and cluster IP info
474+ switch service .Spec .Type {
475+ case "NodePort" :
476+ nodePortServices = append (nodePortServices , fmt .Sprintf ("%s (NodePort: %s)" , serviceLine , service .Spec .ClusterIP ))
477+ case "LoadBalancer" :
478+ externalIP := "<pending>"
479+ if len (service .Status .LoadBalancer .Ingress ) > 0 {
480+ if service .Status .LoadBalancer .Ingress [0 ].IP != "" {
481+ externalIP = service .Status .LoadBalancer .Ingress [0 ].IP
482+ } else if service .Status .LoadBalancer .Ingress [0 ].Hostname != "" {
483+ externalIP = service .Status .LoadBalancer .Ingress [0 ].Hostname
484+ }
485+ }
486+ loadBalancerServices = append (loadBalancerServices , fmt .Sprintf ("%s (LoadBalancer: %s)" , serviceLine , externalIP ))
487+ default :
488+ if service .Spec .ClusterIP == "None" {
489+ headlessServices = append (headlessServices , fmt .Sprintf ("%s (Headless)" , serviceLine ))
490+ } else {
491+ clusterIPServices = append (clusterIPServices , fmt .Sprintf ("%s (ClusterIP: %s)" , serviceLine , service .Spec .ClusterIP ))
492+ }
493+ }
494+ }
495+
496+ // Output organized by service type
497+ if len (clusterIPServices ) > 0 {
498+ result += " ClusterIP Services:\n "
499+ for _ , svc := range clusterIPServices {
500+ result += fmt .Sprintf (" %s\n " , svc )
501+ }
502+ result += "\n "
503+ }
504+
505+ if len (nodePortServices ) > 0 {
506+ result += " NodePort Services:\n "
507+ for _ , svc := range nodePortServices {
508+ result += fmt .Sprintf (" %s\n " , svc )
509+ }
510+ result += "\n "
511+ }
512+
513+ if len (loadBalancerServices ) > 0 {
514+ result += " LoadBalancer Services:\n "
515+ for _ , svc := range loadBalancerServices {
516+ result += fmt .Sprintf (" %s\n " , svc )
517+ }
518+ result += "\n "
519+ }
520+
521+ if len (headlessServices ) > 0 {
522+ result += " Headless Services:\n "
523+ for _ , svc := range headlessServices {
524+ result += fmt .Sprintf (" %s\n " , svc )
525+ }
526+ result += "\n "
527+ }
528+
529+ result += "Next step: Use 'get-pods-by-service' to find pods backing any of these services\n "
530+ result += " Example: get-pods-by-service --namespace " + namespace + " --service <service-name>\n "
531+
532+ return result , nil
533+ }
534+
535+ // GetPodsByService finds pods backing a specific Kubernetes service
536+ func (i * Istio ) GetPodsByService (ctx context.Context , namespace , serviceName string ) (string , error ) {
537+ // Get the service to find its selector
538+ service , err := i .kubeClient .CoreV1 ().Services (namespace ).Get (ctx , serviceName , metav1.GetOptions {})
539+ if err != nil {
540+ return "" , fmt .Errorf ("failed to get service %s: %w" , serviceName , err )
541+ }
542+
543+ result := fmt .Sprintf ("Pods backing service '%s' in namespace '%s':\n \n " , serviceName , namespace )
544+
545+ // Handle headless services or services without selectors
546+ if service .Spec .Selector == nil {
547+ result += fmt .Sprintf (" Service '%s' has no selector - this is likely:\n " , serviceName )
548+ result += " - A headless service with manual endpoints\n "
549+ result += " - An external service (ExternalName type)\n "
550+ result += " - A service with manually configured endpoints\n \n "
551+
552+ // Try to get endpoints to show what's configured
553+ endpoints , err := i .kubeClient .CoreV1 ().Endpoints (namespace ).Get (ctx , serviceName , metav1.GetOptions {})
554+ if err == nil && len (endpoints .Subsets ) > 0 {
555+ result += " Configured endpoints:\n "
556+ for _ , subset := range endpoints .Subsets {
557+ for _ , addr := range subset .Addresses {
558+ if addr .TargetRef != nil && addr .TargetRef .Kind == "Pod" {
559+ result += fmt .Sprintf (" - Pod: %s (IP: %s)\n " , addr .TargetRef .Name , addr .IP )
560+ } else {
561+ result += fmt .Sprintf (" - IP: %s\n " , addr .IP )
562+ }
563+ }
564+ }
565+ }
566+ return result , nil
567+ }
568+
569+ // Convert selector to label selector string
570+ var selectorParts []string
571+ for key , value := range service .Spec .Selector {
572+ selectorParts = append (selectorParts , fmt .Sprintf ("%s=%s" , key , value ))
573+ }
574+ labelSelector := strings .Join (selectorParts , "," )
575+
576+ // Find pods matching the service selector
577+ pods , err := i .kubeClient .CoreV1 ().Pods (namespace ).List (ctx , metav1.ListOptions {
578+ LabelSelector : labelSelector ,
579+ })
580+ if err != nil {
581+ return "" , fmt .Errorf ("failed to list pods for service %s: %w" , serviceName , err )
582+ }
583+
584+ // Separate running and non-running pods
585+ var runningPods []v1.Pod
586+ var nonRunningPods []v1.Pod
587+
588+ for _ , pod := range pods .Items {
589+ if pod .Status .Phase == "Running" {
590+ runningPods = append (runningPods , pod )
591+ } else {
592+ nonRunningPods = append (nonRunningPods , pod )
593+ }
594+ }
595+
596+ result += fmt .Sprintf (" Service selector: %s\n " , labelSelector )
597+ result += fmt .Sprintf (" Total pods found: %d (%d running, %d not running)\n \n " ,
598+ len (pods .Items ), len (runningPods ), len (nonRunningPods ))
599+
600+ // Show running pods (most important)
601+ if len (runningPods ) > 0 {
602+ result += fmt .Sprintf (" Running pods (%d) - Ready for proxy commands:\n " , len (runningPods ))
603+ for _ , pod := range runningPods {
604+ // Check if it has Istio sidecar
605+ hasIstio := false
606+ for _ , container := range pod .Spec .Containers {
607+ if container .Name == "istio-proxy" {
608+ hasIstio = true
609+ break
610+ }
611+ }
612+
613+ readyIcon := "❌"
614+ if isPodReady (pod ) {
615+ readyIcon = "✅"
616+ }
617+
618+ istioIcon := "🔗"
619+ if hasIstio {
620+ istioIcon = "🕸️"
621+ }
622+
623+ result += fmt .Sprintf (" %s %s %s\n " , readyIcon , istioIcon , pod .Name )
624+ result += fmt .Sprintf (" IP: %-15s Node: %s\n " , pod .Status .PodIP , pod .Spec .NodeName )
625+
626+ // Show main application containers (exclude istio-proxy)
627+ var appContainers []string
628+ for _ , container := range pod .Spec .Containers {
629+ if container .Name != "istio-proxy" {
630+ appContainers = append (appContainers , container .Name )
631+ }
632+ }
633+ result += fmt .Sprintf (" Containers: %s\n " , strings .Join (appContainers , ", " ))
634+
635+ if hasIstio {
636+ result += fmt .Sprintf (" 🕸️ Istio mesh: ENABLED\n " )
637+ } else {
638+ result += fmt .Sprintf (" ⚠️ Istio mesh: NOT ENABLED\n " )
639+ }
640+ result += "\n "
641+ }
642+ }
643+
644+ // Show non-running pods for completeness
645+ if len (nonRunningPods ) > 0 {
646+ result += fmt .Sprintf ("⏳ Non-running pods (%d):\n " , len (nonRunningPods ))
647+ for _ , pod := range nonRunningPods {
648+ result += fmt .Sprintf (" ❌ %s (Status: %s)\n " , pod .Name , pod .Status .Phase )
649+ }
650+ result += "\n "
651+ }
652+
653+ if len (runningPods ) == 0 {
654+ result += "⚠️ No running pods found backing this service!\n "
655+ result += "💡 This could mean:\n "
656+ result += " - The deployment is scaled to 0 replicas\n "
657+ result += " - Pods are failing to start\n "
658+ result += " - Label selector mismatch between service and pods\n \n "
659+ return result , nil
660+ }
661+
662+ // Add helpful next steps
663+ result += "💡 Next steps - Use these pod names with proxy commands:\n "
664+ if len (runningPods ) > 0 {
665+ examplePod := runningPods [0 ].Name
666+ result += fmt .Sprintf (" get-proxy-status --namespace %s --pod %s\n " , namespace , examplePod )
667+ result += fmt .Sprintf (" get-proxy-clusters --namespace %s --pod %s\n " , namespace , examplePod )
668+ result += fmt .Sprintf (" get-proxy-listeners --namespace %s --pod %s\n " , namespace , examplePod )
669+ result += fmt .Sprintf (" get-proxy-routes --namespace %s --pod %s\n " , namespace , examplePod )
670+ }
671+
672+ return result , nil
673+ }
674+
675+ // Helper function to check if pod is ready (already exists but ensuring it's here)
676+ func isPodReady (pod v1.Pod ) bool {
677+ for _ , condition := range pod .Status .Conditions {
678+ if condition .Type == v1 .PodReady {
679+ return condition .Status == v1 .ConditionTrue
680+ }
681+ }
682+ return false
683+ }
684+
447685// DiscoverNamespacesWithSidecars finds namespaces that have pods with Istio sidecars
448686// and returns them sorted by the number of sidecars (most injected first)
449687func (i * Istio ) DiscoverNamespacesWithSidecars (ctx context.Context ) (string , error ) {
0 commit comments