Cluster Resource Watcher
A Kubernetes client tool that demonstrates three core client-go patterns: listing pods across all namespaces, creating a ConfigMap resource, and watching for pod changes in real-time using a shared informer. It auto-detects whether it's running in-cluster or locally (via ~/.kube/config) and handles graceful shutdown on Ctrl+C.
Setup
mkdir cluster-resource-watcher
cd cluster-resource-watcher
go mod init github.com/yourorg/cluster-resource-watcher
go get k8s.io/client-go@latest
go get k8s.io/apimachinery@latest
go get k8s.io/api@latestmain.go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
// buildClient returns a Clientset using in-cluster config when running inside
// a pod, and falls back to ~/.kube/config for local development.
func buildClient() (*kubernetes.Clientset, error) {
config, err := rest.InClusterConfig()
if err != nil {
kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
}
return kubernetes.NewForConfig(config)
}
func main() {
clientset, err := buildClient()
if err != nil {
log.Fatalf("building client: %v", err)
}
// 1. List existing pods across all namespaces
fmt.Println("=== Current Pods ===")
pods, err := clientset.CoreV1().Pods("").List(
context.Background(), metav1.ListOptions{},
)
if err != nil {
log.Fatalf("listing pods: %v", err)
}
for _, pod := range pods.Items {
fmt.Printf(" %s/%s [%s]\n", pod.Namespace, pod.Name, pod.Status.Phase)
}
// 2. Create a ConfigMap
fmt.Println("\n=== Creating ConfigMap ===")
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "devops-tool-config",
Namespace: "default",
},
Data: map[string]string{
"version": "1.0.0",
"log_level": "debug",
},
}
created, err := clientset.CoreV1().ConfigMaps("default").Create(
context.Background(), cm, metav1.CreateOptions{},
)
if err != nil {
log.Printf("create configmap: %v (may already exist)", err)
} else {
fmt.Printf(" Created: %s (uid: %s)\n", created.Name, created.UID)
}
// 3. Watch pods with a shared informer
fmt.Println("\n=== Watching Pods (Ctrl+C to stop) ===")
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
stopCh := make(chan struct{})
go func() {
<-ctx.Done()
close(stopCh)
}()
factory := informers.NewSharedInformerFactory(clientset, 60*time.Second)
podInformer := factory.Core().V1().Pods().Informer()
podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod, ok := obj.(*corev1.Pod)
if !ok {
return
}
fmt.Printf(" [+] %s/%s\n", pod.Namespace, pod.Name)
},
DeleteFunc: func(obj interface{}) {
// Objects deleted during a connectivity gap arrive wrapped in
// DeletedFinalStateUnknown. Unwrap before type-asserting.
if d, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = d.Obj
}
pod, ok := obj.(*corev1.Pod)
if !ok {
return
}
fmt.Printf(" [-] %s/%s\n", pod.Namespace, pod.Name)
},
})
factory.Start(stopCh)
if ok := cache.WaitForCacheSync(stopCh, podInformer.HasSynced); !ok {
log.Fatal("failed to sync informer cache")
}
fmt.Println(" watching... (Ctrl+C to stop)")
<-stopCh
fmt.Println("\nShutting down.")
}Running It
# requires a running cluster and a valid kubeconfig
go run main.goExpected output:
=== Current Pods ===
default/api-abc123 [Running]
default/worker-def456 [Running]
=== Creating ConfigMap ===
Created: devops-tool-config (uid: 7a3f2c91-4e8b-11ee-be56-0242ac120002)
=== Watching Pods (Ctrl+C to stop) ===
watching... (Ctrl+C to stop)
[+] default/api-xyz789
[-] default/api-abc123