mirror of
https://github.com/imjasonh/client-go2
synced 2026-07-08 00:55:56 +00:00
This PR adds support for resource-specific expansion methods and implements
missing standard client-go operations to achieve feature parity.
## New Features
### Expansion Methods
- Added PodClient with methods: GetLogs, Bind, Evict, ProxyGet
- Added ServiceClient with method: ProxyGet
- Both clients implement the official k8s.io/client-go expansion interfaces
- Runtime type assertions ensure type safety (panic if wrong type)
### Missing Standard Methods
- Watch: Watch resources with optional label/field selectors
- DeleteCollection: Delete multiple resources matching criteria
- UpdateStatus: Update only the status subresource
### Other Improvements
- Refactored all tests to use inline configuration pattern
- Added comprehensive e2e tests for new functionality
- Updated documentation with examples
- Fixed linting error in stream.Close() handling
## Design Decisions
The expansion methods follow a namespace-scoped pattern where calling
`client.PodClient("namespace")` returns a client that implements the
standard client-go PodExpansion interface. This maintains full API
compatibility while providing type safety through runtime assertions.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
317 lines
9.5 KiB
Go
317 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/imjasonh/client-go2/generic"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
)
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
|
clientcmd.NewDefaultClientConfigLoadingRules(),
|
|
&clientcmd.ConfigOverrides{}).ClientConfig()
|
|
if err != nil {
|
|
log.Fatalf("ClientConfig: %v", err)
|
|
}
|
|
|
|
// List pods in kube-system using automatic GVR inference.
|
|
podClient, err := generic.NewClient[*corev1.Pod](config)
|
|
if err != nil {
|
|
log.Fatal("creating pod client:", err)
|
|
}
|
|
pods, err := podClient.List(ctx, "kube-system", nil)
|
|
if err != nil {
|
|
log.Fatal("listing pods:", err)
|
|
}
|
|
log.Println("LISTING PODS")
|
|
for _, p := range pods {
|
|
log.Println("-", p.Name)
|
|
}
|
|
|
|
// For ConfigMaps, we'll also use automatic GVR inference
|
|
cmc, err := generic.NewClient[*corev1.ConfigMap](config)
|
|
if err != nil {
|
|
log.Fatal("creating configmap client:", err)
|
|
}
|
|
|
|
// Example 1: Start an informer for ALL ConfigMaps (no selector)
|
|
log.Println("Starting informer for ALL ConfigMaps...")
|
|
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
|
|
OnAdd: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[ALL] ConfigMap added: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
|
|
log.Printf("[ALL] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
|
|
},
|
|
OnDelete: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[ALL] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnError: func(obj any, err error) {
|
|
log.Printf("[ALL] Error in ConfigMap informer: %v", err)
|
|
},
|
|
}, nil)
|
|
|
|
// Example 2: Start an informer with label selector
|
|
log.Println("Starting informer for ConfigMaps with label test=example...")
|
|
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
|
|
OnAdd: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[LABELED] ConfigMap added: %s/%s (labels: %v)", obj.Namespace, obj.Name, obj.Labels)
|
|
},
|
|
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
|
|
log.Printf("[LABELED] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
|
|
},
|
|
OnDelete: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[LABELED] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnError: func(obj any, err error) {
|
|
log.Printf("[LABELED] Error: %v", err)
|
|
},
|
|
}, &generic.InformOptions{
|
|
ListOptions: metav1.ListOptions{
|
|
LabelSelector: "test=example",
|
|
},
|
|
})
|
|
|
|
// Example 3: Start a Pod informer with field selector for running pods only
|
|
log.Println("Starting informer for Running Pods only...")
|
|
podClient.Inform(ctx, generic.InformerHandler[*corev1.Pod]{
|
|
OnAdd: func(key string, obj *corev1.Pod) {
|
|
log.Printf("[RUNNING] Pod added: %s/%s (phase: %s)", obj.Namespace, obj.Name, obj.Status.Phase)
|
|
},
|
|
OnUpdate: func(key string, oldObj, newObj *corev1.Pod) {
|
|
if oldObj.Status.Phase != newObj.Status.Phase {
|
|
log.Printf("[RUNNING] Pod phase changed: %s/%s (%s -> %s)",
|
|
oldObj.Namespace, oldObj.Name, oldObj.Status.Phase, newObj.Status.Phase)
|
|
}
|
|
},
|
|
OnDelete: func(key string, obj *corev1.Pod) {
|
|
log.Printf("[RUNNING] Pod deleted: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnError: func(obj any, err error) {
|
|
log.Printf("[RUNNING] Error: %v", err)
|
|
},
|
|
}, &generic.InformOptions{
|
|
ListOptions: metav1.ListOptions{
|
|
FieldSelector: "status.phase=Running",
|
|
},
|
|
})
|
|
|
|
// Example 4: Informer with custom resync period
|
|
resync := 30 * time.Second
|
|
log.Printf("Starting informer with custom resync period of %v...\n", resync)
|
|
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
|
|
OnAdd: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[RESYNC] ConfigMap added: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
|
|
log.Printf("[RESYNC] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
|
|
},
|
|
OnDelete: func(key string, obj *corev1.ConfigMap) {
|
|
log.Printf("[RESYNC] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
|
|
},
|
|
OnError: func(obj any, err error) {
|
|
log.Printf("[RESYNC] Error: %v", err)
|
|
},
|
|
}, &generic.InformOptions{
|
|
ListOptions: metav1.ListOptions{
|
|
LabelSelector: "special=resync-test",
|
|
},
|
|
ResyncPeriod: &resync,
|
|
})
|
|
|
|
// Wait a moment for informers to sync
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Create ConfigMaps with different labels to demonstrate selectors
|
|
log.Println("\nCREATING TEST RESOURCES...")
|
|
|
|
// Create a ConfigMap without labels (will only show in ALL informer)
|
|
cm1, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "no-labels-",
|
|
},
|
|
Data: map[string]string{
|
|
"hello": "world",
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
log.Printf("Error creating cm1: %v", err)
|
|
} else {
|
|
log.Printf("Created ConfigMap without labels: %s", cm1.Name)
|
|
}
|
|
|
|
// Create a ConfigMap with test=example label (will show in LABELED informer)
|
|
cm2, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "labeled-",
|
|
Labels: map[string]string{
|
|
"test": "example",
|
|
},
|
|
},
|
|
Data: map[string]string{
|
|
"hello": "labeled",
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
log.Printf("Error creating cm2: %v", err)
|
|
} else {
|
|
log.Printf("Created ConfigMap with test=example label: %s", cm2.Name)
|
|
}
|
|
|
|
// Create a ConfigMap with special=resync-test label (will show in RESYNC informer)
|
|
cm3, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: "resync-test-",
|
|
Labels: map[string]string{
|
|
"special": "resync-test",
|
|
},
|
|
},
|
|
Data: map[string]string{
|
|
"hello": "resync",
|
|
},
|
|
}, nil)
|
|
if err != nil {
|
|
log.Printf("Error creating cm3: %v", err)
|
|
} else {
|
|
log.Printf("Created ConfigMap with special=resync-test label: %s", cm3.Name)
|
|
}
|
|
|
|
// Wait for create events
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Update the labeled ConfigMap
|
|
if cm2 != nil {
|
|
log.Printf("\nUPDATING CONFIGMAP %s", cm2.Name)
|
|
cm2.Data["hello"] = "updated"
|
|
_, err = cmc.Update(ctx, "default", cm2, nil)
|
|
if err != nil {
|
|
log.Printf("Error updating cm2: %v", err)
|
|
}
|
|
}
|
|
|
|
// Wait for update events
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Clean up
|
|
log.Println("\nCLEANING UP...")
|
|
if cm1 != nil {
|
|
if err := cmc.Delete(ctx, "default", cm1.Name, nil); err != nil {
|
|
log.Printf("Error deleting cm1: %v", err)
|
|
}
|
|
}
|
|
if cm2 != nil {
|
|
if err := cmc.Delete(ctx, "default", cm2.Name, nil); err != nil {
|
|
log.Printf("Error deleting cm2: %v", err)
|
|
}
|
|
}
|
|
if cm3 != nil {
|
|
if err := cmc.Delete(ctx, "default", cm3.Name, nil); err != nil {
|
|
log.Printf("Error deleting cm3: %v", err)
|
|
}
|
|
}
|
|
|
|
// Wait for delete events
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// List ConfigMaps with label selector
|
|
log.Println("\nLISTING CONFIGMAPS WITH LABEL test=example")
|
|
labeledCMs, err := cmc.List(ctx, "", &metav1.ListOptions{
|
|
LabelSelector: "test=example",
|
|
})
|
|
if err != nil {
|
|
log.Printf("Error listing labeled configmaps: %v", err)
|
|
} else {
|
|
log.Printf("Found %d ConfigMaps with test=example label:", len(labeledCMs))
|
|
for _, cm := range labeledCMs {
|
|
log.Printf("- %s/%s", cm.Namespace, cm.Name)
|
|
}
|
|
}
|
|
|
|
// List all ConfigMaps in kube-system
|
|
log.Println("\nLISTING ALL CONFIGMAPS IN kube-system")
|
|
cms, err := cmc.List(ctx, "kube-system", nil)
|
|
if err != nil {
|
|
log.Fatal("listing configmaps:", err)
|
|
}
|
|
log.Printf("Found %d ConfigMaps in kube-system:", len(cms))
|
|
for _, cm := range cms {
|
|
log.Println("-", cm.Name)
|
|
}
|
|
|
|
// Demonstrate Pod expansion methods
|
|
log.Println("\n=== POD EXPANSION EXAMPLE ===")
|
|
|
|
// Get a namespace-scoped PodClient with expansion methods from the existing pod client
|
|
expandedPodClient := podClient.PodClient("kube-system")
|
|
|
|
// List pods to find one to get logs from
|
|
dnsPods, err := podClient.List(ctx, "kube-system", &metav1.ListOptions{
|
|
LabelSelector: "k8s-app=kube-dns",
|
|
})
|
|
if err != nil {
|
|
log.Printf("Error listing pods: %v", err)
|
|
} else if len(dnsPods) > 0 {
|
|
// Get logs from the first CoreDNS pod
|
|
pod := dnsPods[0]
|
|
log.Printf("\nGETTING LOGS FROM POD %s", pod.Name)
|
|
|
|
// Get last 5 lines of logs
|
|
tailLines := int64(5)
|
|
logOpts := &corev1.PodLogOptions{
|
|
TailLines: &tailLines,
|
|
}
|
|
|
|
req := expandedPodClient.GetLogs(pod.Name, logOpts)
|
|
logs, err := req.DoRaw(ctx)
|
|
if err != nil {
|
|
log.Printf("Error getting logs: %v", err)
|
|
} else {
|
|
log.Println("Last 5 lines of logs:")
|
|
log.Println(string(logs))
|
|
}
|
|
|
|
// If the pod has multiple containers, get logs from a specific container
|
|
if len(pod.Spec.Containers) > 0 {
|
|
containerName := pod.Spec.Containers[0].Name
|
|
log.Printf("\nGETTING LOGS FROM CONTAINER %s", containerName)
|
|
|
|
containerLogOpts := &corev1.PodLogOptions{
|
|
Container: containerName,
|
|
TailLines: &tailLines,
|
|
}
|
|
|
|
req := expandedPodClient.GetLogs(pod.Name, containerLogOpts)
|
|
logs, err := req.DoRaw(ctx)
|
|
if err != nil {
|
|
log.Printf("Error getting container logs: %v", err)
|
|
} else {
|
|
log.Printf("Last 5 lines from container %s:", containerName)
|
|
log.Println(string(logs))
|
|
}
|
|
}
|
|
} else {
|
|
log.Println("No CoreDNS pods found to demonstrate GetLogs")
|
|
}
|
|
|
|
// Demonstrate using the generic SubResource method
|
|
log.Println("\n=== SUBRESOURCE EXAMPLE ===")
|
|
if len(dnsPods) > 0 {
|
|
pod := dnsPods[0]
|
|
log.Printf("Getting status subresource for pod %s", pod.Name)
|
|
|
|
req := podClient.SubResource(pod.Namespace, pod.Name, "status")
|
|
statusBytes, err := req.DoRaw(ctx)
|
|
if err != nil {
|
|
log.Printf("Error getting pod status: %v", err)
|
|
} else {
|
|
// Just show that we got data (full status would be verbose)
|
|
log.Printf("Got pod status (%d bytes)", len(statusBytes))
|
|
}
|
|
}
|
|
}
|