1
0
Fork 0
mirror of https://github.com/imjasonh/client-go2 synced 2026-07-15 13:15:32 +00:00
client-go2/generic/e2e_test.go
Jason Hall fb8305a316
Refactor to use rest.Client directly instead of dynamic client
This major refactoring eliminates the dependency on the dynamic client
and unstructured types, using rest.RESTClient directly for all operations.

Key changes:
- Replace dynamic.Interface with rest.RESTClient in client struct
- Remove all unstructured.Unstructured conversions
- Use direct JSON marshaling/unmarshaling for type conversions
- Update all CRUD methods (List, Get, Create, Update, Delete, Patch)
- Reimplement Inform to use cache.ListWatch with rest client
- Export Client type and update method signatures
- Add proper GroupVersion and APIPath configuration in NewClientGVR
- Update all tests to use mock HTTP transport instead of fake dynamic client
- Fix informer event handler to use new InformerHandler type
- Update example code to demonstrate new API

Benefits:
- More efficient: eliminates unnecessary type conversions
- Cleaner code: direct use of REST client
- Better performance: reduced JSON marshaling overhead
- Type safety maintained through generics

All unit tests and e2e tests pass against a real Kubernetes cluster.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-27 16:33:45 -04:00

291 lines
7.2 KiB
Go

//go:build e2e
// +build e2e
package generic
import (
"context"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
)
// TestInferGVRE2E tests GVR inference against a real Kubernetes cluster
func TestInferGVRE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
tests := []struct {
name string
inferFunc func() (schema.GroupVersionResource, error)
expectedGVR schema.GroupVersionResource
}{
{
name: "Pod",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Pod](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
},
},
{
name: "ConfigMap",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.ConfigMap](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
},
},
{
name: "Service",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Service](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "services",
},
},
{
name: "Secret",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Secret](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
},
},
{
name: "Namespace",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Namespace](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "namespaces",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gvr, err := tt.inferFunc()
if err != nil {
t.Fatalf("failed to infer GVR: %v", err)
}
if gvr != tt.expectedGVR {
t.Errorf("expected GVR %v, got %v", tt.expectedGVR, gvr)
}
})
}
}
// TestNewClientE2E tests client creation with inferred GVR against a real cluster
func TestNewClientE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
ctx := context.Background()
t.Run("Pod client operations", func(t *testing.T) {
client, err := NewClient[*corev1.Pod](config)
if err != nil {
t.Fatalf("failed to create pod client: %v", err)
}
// Try to list pods in default namespace
pods, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list pods: %v", err)
}
t.Logf("Successfully listed %d pods in default namespace", len(pods))
})
t.Run("ConfigMap client operations", func(t *testing.T) {
client, err := NewClient[*corev1.ConfigMap](config)
if err != nil {
t.Fatalf("failed to create configmap client: %v", err)
}
// Try to list configmaps in default namespace
cms, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list configmaps: %v", err)
}
t.Logf("Successfully listed %d configmaps in default namespace", len(cms))
})
t.Run("Service client operations", func(t *testing.T) {
client, err := NewClient[*corev1.Service](config)
if err != nil {
t.Fatalf("failed to create service client: %v", err)
}
// Try to list services in default namespace
svcs, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list services: %v", err)
}
t.Logf("Successfully listed %d services in default namespace", len(svcs))
})
}
// TestInferGVRErrorCases tests error cases for GVR inference
func TestInferGVRErrorCases(t *testing.T) {
config := &rest.Config{
Host: "http://localhost:8080",
}
t.Run("Unregistered type", func(t *testing.T) {
// This should fail because no scheme is registered for this type
type UnregisteredType struct {
*corev1.Pod
}
_, err := inferGVR[*UnregisteredType](config)
if err == nil {
t.Error("expected error for unregistered type, got nil")
}
})
}
// TestInformE2E tests informer functionality against a real cluster
func TestInformE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Create a ConfigMap client with inferred GVR
client, err := NewClient[*corev1.ConfigMap](config)
if err != nil {
t.Fatalf("failed to create configmap client: %v", err)
}
// Track events
events := make(chan string, 100)
handler := InformerHandler[*corev1.ConfigMap]{
OnAdd: func(key string, obj *corev1.ConfigMap) {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
if namespace == "default" {
events <- "add:" + name
}
},
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
if namespace == "default" {
events <- "update:" + name
}
},
OnDelete: func(key string, obj *corev1.ConfigMap) {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
if namespace == "default" {
events <- "delete:" + name
}
},
OnError: func(obj any, err error) {
t.Logf("Informer error: %v for object %v", err, obj)
},
}
client.Inform(ctx, handler)
// Create a test ConfigMap
testCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-inform-" + time.Now().Format("20060102-150405"),
Namespace: "default",
},
Data: map[string]string{
"test": "data",
},
}
created, err := client.Create(ctx, "default", testCM)
if err != nil {
t.Fatalf("failed to create test configmap: %v", err)
}
testCM = created
defer func() {
// Clean up
if err := client.Delete(ctx, "default", testCM.Name); err != nil {
t.Logf("failed to delete test configmap: %v", err)
}
}()
// Wait for add event for our specific ConfigMap
deadline := time.After(10 * time.Second)
foundAdd := false
for !foundAdd {
select {
case event := <-events:
t.Logf("Received event: %s", event)
if event == "add:"+testCM.Name {
foundAdd = true
}
case <-deadline:
t.Fatal("timeout waiting for add event")
}
}
// Update the ConfigMap
testCM.Data["test"] = "updated"
updated, err := client.Update(ctx, "default", testCM)
if err != nil {
t.Fatalf("failed to update test configmap: %v", err)
}
testCM = updated
// Wait for update event
deadline = time.After(10 * time.Second)
foundUpdate := false
for !foundUpdate {
select {
case event := <-events:
t.Logf("Received event: %s", event)
if event == "update:"+testCM.Name {
foundUpdate = true
}
case <-deadline:
t.Fatal("timeout waiting for update event")
}
}
}