Why Go Is Ideal for Ops Tools
Ops CLI tools demand high deployment convenience and execution efficiency — areas where Go has natural advantages:
| Advantage | Description | Compared to Python |
|---|---|---|
| Single binary | Compiles to a standalone executable with no runtime dependencies | Requires Python environment + dependencies |
| Cross-platform | GOOS/GOARCH cross-compilation — write once, run anywhere | Requires virtualenv management |
| Startup speed | Millisecond-level cold start | Interpreter overhead |
| Concurrency model | Lightweight goroutines | Requires threading/asyncio |
| Memory footprint | Low memory usage as a static binary | Interpreter overhead |
| Mature ecosystem | Standard library covers networking/files/crypto | Relies on third-party libraries |
Kubernetes, Docker, Terraform, Prometheus — the core cloud-native tools are all written in Go. Go has become the de facto standard language for cloud-native ops tooling.
Getting Started with cobra
cobra is the most popular CLI framework in the Go ecosystem, providing command trees, flag parsing, auto-completion, and more. According to the cobra documentation, its design philosophy is composition over inheritance.
Installation and Project Initialization
# Install cobra CLI tool
go install github.com/spf13/cobra/cobra@latest
# Initialize project
mkdir k8s-pod-cleaner && cd k8s-pod-cleaner
go mod init github.com/xubaojin/k8s-pod-cleaner
# Generate project scaffold with cobra
cobra init --author "Xu Baojin" --license mit --package cmd
# Add subcommands
cobra add list
cobra add clean
cobra add logs
Command Tree Design
A well-designed CLI tool should have a clear command hierarchy:
k8s-pod-cleaner
├── list # List abnormal Pods
│ └── --status # Filter by status
├── clean # Batch cleanup
│ └── --force # Force delete
├── logs # Log export
│ └── --tail # Line count limit
└── version # Version info
Root Command and Persistent Flags
// cmd/root.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var kubeconfig string
var namespace string
var rootCmd = &cobra.Command{
Use: "k8s-pod-cleaner",
Short: "K8s abnormal Pod management tool",
Long: `k8s-pod-cleaner is a CLI tool for managing abnormal Pods in Kubernetes clusters.
Features:
- List Pods in abnormal states (CrashLoopBackOff, ImagePullBackOff, etc.)
- Batch clean up abnormal Pods
- Export logs from abnormal Pods`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func init() {
// PersistentFlag applies to all subcommands
rootCmd.PersistentFlags().StringVarP(
&kubeconfig, "kubeconfig", "k", "",
"path to kubeconfig file (defaults to ~/.kube/config)",
)
rootCmd.PersistentFlags().StringVarP(
&namespace, "namespace", "n", "",
"Kubernetes namespace (defaults to all namespaces)",
)
}
The distinction between PersistentFlag vs Local Flag is a core cobra concept:
PersistentFlags(): The flag is visible to the current command and all subcommandsFlags(): The flag is only visible to the current command
Subcommands and Flags
// cmd/list.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var statusFilter string
var listCmd = &cobra.Command{
Use: "list",
Short: "List Pods in abnormal states",
Run: func(cmd *cobra.Command, args []string) {
client, err := NewK8sClient(kubeconfig)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Failed to connect to cluster: %v\n", err)
return
}
pods, err := client.ListAbnormalPods(namespace, statusFilter)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Query failed: %v\n", err)
return
}
printPodTable(pods)
},
}
func init() {
listCmd.Flags().StringVarP(
&statusFilter, "status", "s", "",
"filter by status: CrashLoopBackOff, ImagePullBackOff, Pending, Failed",
)
rootCmd.AddCommand(listCmd)
}
Building the k8s-pod-cleaner CLI Tool
K8s Client Wrapper
// pkg/k8s/client.go
package k8s
import (
"context"
"fmt"
"path/filepath"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
type PodInfo struct {
Namespace string
Name string
Status string
Restarts int32
Age string
Node string
Reason string
}
type Client struct {
clientset *kubernetes.Clientset
}
func NewClient(kubeconfig string) (*Client, error) {
if kubeconfig == "" {
if home := homedir.HomeDir(); home != "" {
kubeconfig = filepath.Join(home, ".kube", "config")
}
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create clientset: %w", err)
}
return &Client{clientset: clientset}, nil
}
// ListAbnormalPods lists Pods in abnormal states
func (c *Client) ListAbnormalPods(namespace, statusFilter string) ([]PodInfo, error) {
ctx := context.Background()
var pods []PodInfo
nsList := []string{namespace}
if namespace == "" {
nsList = nil // nil means all namespaces
}
if namespace != "" {
podList, err := c.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, pod := range podList.Items {
if info := c.checkAbnormal(pod, statusFilter); info != nil {
pods = append(pods, *info)
}
}
} else {
// List across all namespaces
podList, err := c.clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, pod := range podList.Items {
if info := c.checkAbnormal(pod, statusFilter); info != nil {
pods = append(pods, *info)
}
}
}
return pods, nil
}
func (c *Client) checkAbnormal(pod corev1.Pod, filter string) *PodInfo {
status := string(pod.Status.Phase)
reason := ""
// Check container statuses
for _, cs := range pod.Status.ContainerStatuses {
if cs.State.Waiting != nil {
reason = cs.State.Waiting.Reason
status = cs.State.Waiting.Reason // CrashLoopBackOff, ImagePullBackOff, etc.
}
if cs.State.Terminated != nil {
reason = cs.State.Terminated.Reason
status = "Terminated"
}
}
// Abnormal status detection
abnormal := map[string]bool{
"CrashLoopBackOff": true,
"ImagePullBackOff": true,
"ErrImagePull": true,
"Pending": true,
"Failed": true,
"Terminated": true,
}
if !abnormal[status] {
return nil
}
// Status filter
if filter != "" && filter != status {
return nil
}
var restarts int32
if len(pod.Status.ContainerStatuses) > 0 {
restarts = pod.Status.ContainerStatuses[0].RestartCount
}
return &PodInfo{
Namespace: pod.Namespace,
Name: pod.Name,
Status: status,
Restarts: restarts,
Node: pod.Spec.NodeName,
Reason: reason,
}
}
// DeletePod deletes the specified Pod
func (c *Client) DeletePod(namespace, name string, force bool) error {
ctx := context.Background()
deleteOpts := metav1.DeleteOptions{}
if force {
gracePeriod := int64(0)
deleteOpts.GracePeriodSeconds = &gracePeriod
}
return c.clientset.CoreV1().Pods(namespace).Delete(ctx, name, deleteOpts)
}
// GetPodLogs retrieves Pod logs
func (c *Client) GetPodLogs(namespace, name string, tailLines int64) (string, error) {
ctx := context.Background()
opts := &corev1.PodLogOptions{
TailLines: &tailLines,
}
req := c.clientset.CoreV1().Pods(namespace).GetLogs(name, opts)
logs, err := req.Stream(ctx)
if err != nil {
return "", err
}
defer logs.Close()
buf := make([]byte, 0, 4096)
tmp := make([]byte, 4096)
for {
n, err := logs.Read(tmp)
if n > 0 {
buf = append(buf, tmp[:n]...)
}
if err != nil {
break
}
}
return string(buf), nil
}
Clean Subcommand
// cmd/clean.go
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var forceDelete bool
var dryRun bool
var cleanCmd = &cobra.Command{
Use: "clean",
Short: "Batch clean up abnormal Pods",
Run: func(cmd *cobra.Command, args []string) {
client, err := NewK8sClient(kubeconfig)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Failed to connect to cluster: %v\n", err)
return
}
pods, err := client.ListAbnormalPods(namespace, "")
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Query failed: %v\n", err)
return
}
if len(pods) == 0 {
fmt.Println("No abnormal Pods to clean up")
return
}
fmt.Printf("Found %d abnormal Pods:\n", len(pods))
for _, p := range pods {
fmt.Printf(" %s/%s [%s] restarts=%d\n", p.Namespace, p.Name, p.Status, p.Restarts)
}
if dryRun {
fmt.Println("\n--dry-run mode, no actual deletion performed")
return
}
success := 0
failed := 0
for _, p := range pods {
err := client.DeletePod(p.Namespace, p.Name, forceDelete)
if err != nil {
fmt.Printf(" ✗ %s/%s deletion failed: %v\n", p.Namespace, p.Name, err)
failed++
} else {
fmt.Printf(" ✓ %s/%s deleted\n", p.Namespace, p.Name)
success++
}
}
fmt.Printf("\nCleanup complete: %d succeeded, %d failed\n", success, failed)
},
}
func init() {
cleanCmd.Flags().BoolVarP(&forceDelete, "force", "f", false, "force delete (gracePeriod=0)")
cleanCmd.Flags().BoolVar(&dryRun, "dry-run", false, "show Pods to be deleted without actually deleting")
rootCmd.AddCommand(cleanCmd)
}
Logs Subcommand
// cmd/logs.go
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
var tailLines int64
var outputDir string
var logsCmd = &cobra.Command{
Use: "logs",
Short: "Export logs from abnormal Pods",
Run: func(cmd *cobra.Command, args []string) {
client, err := NewK8sClient(kubeconfig)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Failed to connect to cluster: %v\n", err)
return
}
pods, err := client.ListAbnormalPods(namespace, "")
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Query failed: %v\n", err)
return
}
if outputDir == "" {
outputDir = "./pod-logs"
}
if err := os.MkdirAll(outputDir, 0755); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Failed to create output directory: %v\n", err)
return
}
for _, p := range pods {
logs, err := client.GetPodLogs(p.Namespace, p.Name, tailLines)
if err != nil {
fmt.Printf(" ✗ %s/%s log retrieval failed: %v\n", p.Namespace, p.Name, err)
continue
}
filename := filepath.Join(outputDir, fmt.Sprintf("%s_%s.log", p.Namespace, p.Name))
if err := os.WriteFile(filename, []byte(logs), 0644); err != nil {
fmt.Printf(" ✗ %s write failed: %v\n", filename, err)
continue
}
fmt.Printf(" ✓ %s/%s → %s\n", p.Namespace, p.Name, filename)
}
},
}
func init() {
logsCmd.Flags().Int64VarP(&tailLines, "tail", "t", 100, "get last N lines of logs")
logsCmd.Flags().StringVarP(&outputDir, "output", "o", "", "log output directory (defaults to ./pod-logs)")
rootCmd.AddCommand(logsCmd)
}
Output Formatting
// cmd/output.go
package cmd
import (
"fmt"
"os"
"text/tabwriter"
)
func printPodTable(pods []PodInfo) {
if len(pods) == 0 {
fmt.Println("No abnormal Pods")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAMESPACE\tNAME\tSTATUS\tRESTARTS\tNODE\tREASON")
for _, p := range pods {
fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\n",
p.Namespace, p.Name, p.Status, p.Restarts, p.Node, p.Reason)
}
w.Flush()
fmt.Printf("\nTotal: %d abnormal Pods\n", len(pods))
}
Usage Examples
# List abnormal Pods across all namespaces
k8s-pod-cleaner list
# List only CrashLoopBackOff Pods
k8s-pod-cleaner list -s CrashLoopBackOff
# Specify a namespace
k8s-pod-cleaner list -n production
# Dry-run cleanup (no actual deletion)
k8s-pod-cleaner clean --dry-run
# Force clean all abnormal Pods
k8s-pod-cleaner clean -f
# Export abnormal Pod logs (last 200 lines)
k8s-pod-cleaner logs -t 200 -o ./incident-logs
Cross-Platform Compilation and Version Management
Cross-Compilation
# Linux amd64
GOOS=linux GOARCH=amd64 go build -o bin/k8s-pod-cleaner-linux-amd64
# macOS arm64 (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o bin/k8s-pod-cleaner-darwin-arm64
# Windows amd64
GOOS=windows GOARCH=amd64 go build -o bin/k8s-pod-cleaner-windows-amd64.exe
Version Information Injection
// cmd/version.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var (
Version = "dev"
Commit = "none"
BuildDate = "unknown"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Show version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("k8s-pod-cleaner %s\n", Version)
fmt.Printf(" commit: %s\n", Commit)
fmt.Printf(" build: %s\n", BuildDate)
fmt.Printf(" go: %s\n", runtime.Version())
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
Inject version info at build time:
go build -ldflags "\
-X github.com/xubaojin/k8s-pod-cleaner/cmd.Version=v1.0.0 \
-X github.com/xubaojin/k8s-pod-cleaner/cmd.Commit=$(git rev-parse --short HEAD) \
-X github.com/xubaojin/k8s-pod-cleaner/cmd.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o bin/k8s-pod-cleaner
GoReleaser Automated Release
GoReleaser is a release automation tool for Go projects. It handles cross-compilation, packaging, changelog generation, and GitHub Releases in one step:
# .goreleaser.yaml
version: 2
project_name: k8s-pod-cleaner
before:
hooks:
- go mod tidy
builds:
- id: k8s-pod-cleaner
main: ./main.go
binary: k8s-pod-cleaner
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w
- -X github.com/xubaojin/k8s-pod-cleaner/cmd.Version={{.Version}}
- -X github.com/xubaojin/k8s-pod-cleaner/cmd.Commit={{.ShortCommit}}
- -X github.com/xubaojin/k8s-pod-cleaner/cmd.BuildDate={{.Date}}
archives:
- id: default
formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
name_template: >-
{{ .ProjectName }}_{{ .Version }}_
{{- if eq .Os "darwin" }}macOS
{{- else if eq .Os "linux" }}Linux
{{- else if eq .Os "windows" }}Windows
{{- end }}_{{ .Arch }}
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
groups:
- title: 'New Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
- title: 'Bug Fixes'
regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
- title: 'Other'
default: true
release:
github:
owner: xubaojin
name: k8s-pod-cleaner
draft: false
prerelease: auto
Execute the release:
# Local test build
goreleaser release --snapshot --clean
# Official release (requires git tag)
git tag v1.0.0
git push origin v1.0.0
goreleaser release --clean
GoReleaser automatically handles:
- Cross-compilation for 6 platforms (linux/darwin/windows × amd64/arm64)
- Generating tar.gz/zip archives
- Generating SHA256 checksum files
- Auto-generating Changelog
- Creating a GitHub Release and uploading artifacts
Summary
Go + cobra is the golden combination for building ops CLI tools. Go’s single-binary nature and cross-platform capabilities solve deployment pain points, while cobra’s command tree design keeps the user experience clear even for complex tools.
Key principles for building ops CLI tools: clean command hierarchy, clear flag semantics, parseable formatted output, and a dry-run mode. Combined with GoReleaser for one-command multi-platform releases, tool distribution and maintenance become effortless.
From kubectl to helm to argocd, the best CLI tools in the cloud-native ecosystem all follow this pattern. Master Go + cobra, and you’ve mastered the core skill for building professional ops tooling.
References & Acknowledgments
This article referenced the following materials during writing. We thank the original authors for their contributions:
- cobra — Cobra, referenced for technical content
- GoReleaser — Goreleaser, referenced for GoReleaser