1
0
Fork 0
mirror of https://github.com/imjasonh/client-go2 synced 2026-07-08 09:05:38 +00:00
client-go2/generic/client.go
Jason Hall d6bb3cc61c
Update client-go2 with enhanced functionality and modern dependencies
- Fix syntax error in Inform method (< to ,)
- Add missing CRUD operations: Update, Delete, and Patch methods
- Add comprehensive test coverage for all client operations
- Update Kubernetes dependencies from v0.21.1 to v0.31.11
- Update Go version requirement to 1.22.0
- Update README to remove outdated Go 1.18 beta references
- Improve error handling and logging

The client now provides full CRUD functionality with type-safe generic
operations for any Kubernetes resource type, significantly reducing
boilerplate compared to traditional client-go usage.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-23 09:39:08 -04:00

139 lines
4.1 KiB
Go

package generic
import (
"bytes"
"context"
"encoding/json"
"log"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
)
const resyncPeriod = time.Hour
// Ideally, this wouldn't need to take a GVR, and it could just be inferred
// from the runtime.Object type given.
//
// In practice, this doesn't seem to be straightforward. :(
//
// Things that implement runtime.Object tend to be pointer types (e.g.,
// *corev1.Pod), which means T is nil, and we can't call GetObjectKind() on it
// to start to guess at the GVR.
//
// It might be possible to use schemes to lookup the GVR for a given Go type
// (assuming it's been registered), which could let us get rid of this.
// In the meantime, taking a GVR removes ambiguity at the cost of verbosity.
// /shrug
func NewClient[T runtime.Object](gvr schema.GroupVersionResource, config *rest.Config) client[T] {
dyn := dynamic.NewForConfigOrDie(config)
return client[T]{
gvr: gvr,
dyn: dyn,
dsif: dynamicinformer.NewDynamicSharedInformerFactory(dyn, resyncPeriod),
}
}
type client[T runtime.Object] struct {
gvr schema.GroupVersionResource
// TODO: don't depend on dynamic client
dyn dynamic.Interface
dsif dynamicinformer.DynamicSharedInformerFactory
}
func (c client[T]) Start(ctx context.Context) {
go c.dsif.Start(ctx.Done())
c.dsif.WaitForCacheSync(ctx.Done())
}
func (c client[T]) List(ctx context.Context, namespace string) ([]T, error) {
ul, err := c.dyn.Resource(c.gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
var out []T
for _, u := range ul.Items {
var t T
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(u.Object); err != nil {
return nil, err
}
if err := json.NewDecoder(&buf).Decode(&t); err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
func (c client[T]) Get(ctx context.Context, namespace, name string) (T, error) {
var t T
u, err := c.dyn.Resource(c.gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return t, err
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(u.Object); err != nil {
return t, err
}
if err := json.NewDecoder(&buf).Decode(&t); err != nil {
return t, err
}
return t, nil
}
func (c client[T]) Create(ctx context.Context, namespace string, t T) error {
m := map[string]interface{}{}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(t); err != nil {
return err
}
if err := json.NewDecoder(&buf).Decode(&m); err != nil {
return err
}
u := &unstructured.Unstructured{Object: m}
_, err := c.dyn.Resource(c.gvr).Namespace(namespace).Create(ctx, u, metav1.CreateOptions{})
return err
}
func (c client[T]) Update(ctx context.Context, namespace string, t T) error {
m := map[string]interface{}{}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(t); err != nil {
return err
}
if err := json.NewDecoder(&buf).Decode(&m); err != nil {
return err
}
u := &unstructured.Unstructured{Object: m}
_, err := c.dyn.Resource(c.gvr).Namespace(namespace).Update(ctx, u, metav1.UpdateOptions{})
return err
}
func (c client[T]) Delete(ctx context.Context, namespace, name string) error {
return c.dyn.Resource(c.gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
func (c client[T]) Patch(ctx context.Context, namespace, name string, pt types.PatchType, data []byte) error {
_, err := c.dyn.Resource(c.gvr).Namespace(namespace).Patch(ctx, name, pt, data, metav1.PatchOptions{})
return err
}
func (c client[T]) Inform(ctx context.Context, handler cache.ResourceEventHandler) {
inf := c.dsif.ForResource(c.gvr).Informer()
inf.AddEventHandler(handler)
go inf.Run(ctx.Done())
if !cache.WaitForNamedCacheSync(c.gvr.String(), ctx.Done(), inf.HasSynced) {
log.Println("Failed to wait for caches to sync:", c.gvr.String())
return
}
}