blog.maisumvictor.dev β€” zsh

Building CLI Tools in Go for Kubernetes Operations

Building CLI Tools in Go for Kubernetes Operations
blog.maisumvictor.dev β€” zsh

Building CLI Tools in Go for Kubernetes Operations

Go is the lingua franca of cloud-native tooling. Let’s build a practical CLI tool that interacts with Kubernetes clusters.

Why Go for Kubernetes Tools?

  • Native Kubernetes client: The official client-go library
  • Single binary: Easy distribution, no dependencies
  • Fast execution: Compiled language beats scripts
  • Rich ecosystem: Cobra, Viper, and more

Project Setup

mkdir k8s-tool && cd k8s-tool
go mod init github.com/maisumvictor/k8s-tool
go get github.com/spf13/cobra@latest
go get k8s.io/[email protected]

Basic Structure

// main.go
package main

import (
	"fmt"
	"os"

	"github.com/maisumvictor/k8s-tool/cmd"
)

func main() {
	if err := cmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

The Root Command

// cmd/root.go
package cmd

import (
	"github.com/spf13/cobra"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

var (
	kubeconfig string
	namespace  string
	clientset  *kubernetes.Clientset
)

var rootCmd = &cobra.Command{
	Use:   "k8s-tool",
	Short: "A CLI tool for Kubernetes operations",
	Long: `k8s-tool provides utilities for common Kubernetes tasks
like checking pod status, cleaning up resources, and generating reports.`,
	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
		return initKubernetesClient()
	},
}

func Execute() error {
	return rootCmd.Execute()
}

func init() {
	rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to kubeconfig file")
	rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Kubernetes namespace")
}

func initKubernetesClient() error {
	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
	if kubeconfig != "" {
		loadingRules.ExplicitPath = kubeconfig
	}

	configOverrides := &clientcmd.ConfigOverrides{}
	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
		loadingRules,
		configOverrides,
	)

	config, err := kubeConfig.ClientConfig()
	if err != nil {
		return fmt.Errorf("failed to create k8s config: %w", err)
	}

	clientset, err = kubernetes.NewForConfig(config)
	if err != nil {
		return fmt.Errorf("failed to create k8s client: %w", err)
	}

	return nil
}

Listing Pods with Status

// cmd/pods.go
package cmd

import (
	"context"
	"fmt"
	"os"
	"text/tabwriter"
	"time"

	"github.com/fatih/color"
	"github.com/spf13/cobra"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var podsCmd = &cobra.Command{
	Use:   "pods",
	Short: "List pods with enhanced status information",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()
		
		pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
		if err != nil {
			return fmt.Errorf("failed to list pods: %w", err)
		}

		// Create tabwriter for aligned output
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
		fmt.Fprintln(w, "NAME\tSTATUS\tRESTARTS\tAGE\tNODE")

		for _, pod := range pods.Items {
			status := getPodStatus(&pod)
			age := time.Since(pod.CreationTimestamp.Time).Round(time.Second)
			
			fmt.Fprintf(w, "%s\t%s\t%d\t%s\t%s\n",
				pod.Name,
				status,
				getRestartCount(&pod),
				formatDuration(age),
				pod.Spec.NodeName,
			)
		}

		return w.Flush()
	},
}

func getPodStatus(pod *corev1.Pod) string {
	phase := pod.Status.Phase
	
	switch phase {
	case corev1.PodRunning:
		// Check if all containers are ready
		allReady := true
		for _, cs := range pod.Status.ContainerStatuses {
			if !cs.Ready {
				allReady = false
				break
			}
		}
		if allReady {
			return color.GreenString("Running")
		}
		return color.YellowString("NotReady")
	case corev1.PodPending:
		return color.YellowString("Pending")
	case corev1.PodFailed:
		return color.RedString("Failed")
	case corev1.PodSucceeded:
		return color.BlueString("Completed")
	default:
		return string(phase)
	}
}

func getRestartCount(pod *corev1.Pod) int32 {
	var total int32
	for _, cs := range pod.Status.ContainerStatuses {
		total += cs.RestartCount
	}
	return total
}

func formatDuration(d time.Duration) string {
	if d.Hours() >= 24 {
		return fmt.Sprintf("%dd", int(d.Hours()/24))
	}
	if d.Hours() >= 1 {
		return fmt.Sprintf("%dh", int(d.Hours()))
	}
	return fmt.Sprintf("%dm", int(d.Minutes()))
}

func init() {
	rootCmd.AddCommand(podsCmd)
}

Resource Cleanup Command

// cmd/cleanup.go
package cmd

import (
	"context"
	"fmt"

	"github.com/spf13/cobra"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
	olderThan string
	dryRun    bool
)

var cleanupCmd = &cobra.Command{
	Use:   "cleanup",
	Short: "Clean up stale Kubernetes resources",
	Long:  `Remove completed pods, failed jobs, and unused configmaps older than specified duration.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()
		
		if dryRun {
			fmt.Println("πŸ” DRY RUN MODE - No resources will be deleted")
		}

		// Clean up completed pods
		pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
			FieldSelector: "status.phase=Succeeded",
		})
		if err != nil {
			return err
		}

		fmt.Printf("Found %d completed pods\n", len(pods.Items))
		
		if !dryRun {
			for _, pod := range pods.Items {
				err := clientset.CoreV1().Pods(namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{})
				if err != nil {
					fmt.Printf("❌ Failed to delete pod %s: %v\n", pod.Name, err)
				} else {
					fmt.Printf("βœ… Deleted pod: %s\n", pod.Name)
				}
			}
		}

		return nil
	},
}

func init() {
	cleanupCmd.Flags().StringVar(&olderThan, "older-than", "24h", "Delete resources older than this duration")
	cleanupCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show what would be deleted without deleting")
	rootCmd.AddCommand(cleanupCmd)
}

Building and Distributing

# Build for current platform
go build -o k8s-tool

# Cross-compile for multiple platforms
GOOS=linux GOARCH=amd64 go build -o k8s-tool-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o k8s-tool-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o k8s-tool-windows-amd64.exe

# Install locally
go install github.com/maisumvictor/k8s-tool@latest

Usage Examples

# List pods in default namespace
k8s-tool pods

# List pods in specific namespace
k8s-tool pods -n kube-system

# Use specific kubeconfig
k8s-tool --kubeconfig ~/.kube/prod pods

# Preview cleanup
k8s-tool cleanup --dry-run

# Actually cleanup
k8s-tool cleanup --older-than 48h

Next Steps

  • Add more commands (jobs, configmaps, secrets)
  • Implement JSON/YAML output for scripting
  • Add completion scripts for bash/zsh
  • Create a Helm plugin wrapper

The complete source code is available on GitHub.