package cmd import ( "context" "fmt" "os" "time" "gitea.e13.dev/e13/kubectl-unready/cmd/version" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" kwatch "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/kubernetes" ) func NewUnreadyCmd(ctx context.Context) *cobra.Command { var kubernetesConfigFlags *genericclioptions.ConfigFlags var allNamespacesFlag bool var watchFlag bool cmd := &cobra.Command{ Use: "unready", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { if kubernetesConfigFlags.Namespace == nil || *kubernetesConfigFlags.Namespace == "" { kubernetesConfigFlags.Namespace = new("default") } return run(ctx, kubernetesConfigFlags, allNamespacesFlag, watchFlag) }, } kubernetesConfigFlags = genericclioptions.NewConfigFlags(false) kubernetesConfigFlags.AddFlags(cmd.Flags()) cmd.Flags().BoolVarP(&allNamespacesFlag, "all-namespaces", "A", false, "If present, list unready pods across all namespaces.") cmd.Flags().BoolVarP(&watchFlag, "watch", "w", false, "Watch for changes to unready pods.") cmd.AddCommand(version.NewVersionCmd(ctx)) return cmd } func run(ctx context.Context, configFlags *genericclioptions.ConfigFlags, allNamespaces bool, doWatch bool) error { config, err := configFlags.ToRESTConfig() if err != nil { return fmt.Errorf("failed to read kubeconfig: %w", err) } clientset, err := kubernetes.NewForConfig(config) if err != nil { return fmt.Errorf("failed to create clientset: %w", err) } namespace := *configFlags.Namespace if allNamespaces { namespace = "" } pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) if err != nil { return fmt.Errorf("failed listing Pods: %w", err) } filtered := make([]corev1.Pod, 0) for _, pod := range pods.Items { if isNotReady(pod) { filtered = append(filtered, pod) } } if err := printPods(filtered, false); err != nil { return err } if !doWatch { return nil } watcher, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{ ResourceVersion: pods.ResourceVersion, }) if err != nil { return fmt.Errorf("failed watching Pods: %w", err) } defer watcher.Stop() for { select { case <-ctx.Done(): return nil case event, ok := <-watcher.ResultChan(): if !ok { return nil } pod, ok := event.Object.(*corev1.Pod) if !ok { continue } if event.Type == kwatch.Added || event.Type == kwatch.Modified { if isNotReady(*pod) { if err := printPods([]corev1.Pod{*pod}, true); err != nil { return err } } } } } } func printPods(pods []corev1.Pod, noHeaders bool) error { table := metav1.Table{ ColumnDefinitions: []metav1.TableColumnDefinition{ {Name: "Namespace"}, {Name: "Name"}, {Name: "Ready"}, {Name: "Status"}, {Name: "Age"}, }, } for _, pod := range pods { table.Rows = append(table.Rows, metav1.TableRow{ Cells: []any{ pod.Namespace, pod.Name, containerStatuses(pod), pod.Status.Phase, duration.HumanDuration(time.Since(pod.CreationTimestamp.Time)), }, }) } printer := printers.NewTablePrinter(printers.PrintOptions{NoHeaders: noHeaders}) return printer.PrintObj(&table, os.Stdout) } func containerStatuses(pod corev1.Pod) string { ready := 0 unready := 0 for _, status := range pod.Status.ContainerStatuses { if status.Ready { ready++ } else { unready++ } } return fmt.Sprintf("%d/%d", ready, ready+unready) } func isNotReady(pod corev1.Pod) bool { return hasUnreadyContainers(pod) && pod.Status.Phase != corev1.PodSucceeded } func hasUnreadyContainers(pod corev1.Pod) bool { for _, ctr := range pod.Status.ContainerStatuses { if !ctr.Ready { return true } } return false }