Skip to content

Commit e9017ee

Browse files
committed
feat: add new tools : get-services and get-pods-by-service
1 parent e334548 commit e9017ee

File tree

3 files changed

+769
-0
lines changed

3 files changed

+769
-0
lines changed

pkg/istio/istio.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
449687
func (i *Istio) DiscoverNamespacesWithSidecars(ctx context.Context) (string, error) {

0 commit comments

Comments
 (0)