Adds a -w/--watch flag that keeps running after the initial list, streaming Added/Modified pod events and printing rows without headers for a continuous view of unready pods.
166 lines
3.8 KiB
Go
166 lines
3.8 KiB
Go
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
|
|
}
|