From 59f526eccf2feea09b667f365ec187ccbda69ae1 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Thu, 9 Sep 2021 16:06:10 -0400 Subject: [PATCH] trying to make the client simpler; doesn't work :( --- README.md | 4 +- generic/client.go | 83 ++++++++++++++++++++++++++++++++ main.go | 117 +++++++--------------------------------------- 3 files changed, 102 insertions(+), 102 deletions(-) create mode 100644 generic/client.go diff --git a/README.md b/README.md index 65ed6c8..a11f545 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Experimenting with `go-client` and Go generics -This is an experimental type-parameter-aware client that wraps [`k8s.io/client-go/dynamic`](https://pkg.go.dev/k8s.io/client-go/dynamic). +This is an experimental type-parameter-aware client that wraps [`k8s.io/client-go/dynamic`](https://pkg.go.dev/k8s.io/client-go/dynamic) _(...for now)_. -Until Go 1.18 is released, use [`gotip`](https://pkg.go.dev/golang.org/dl/gotip) +Until Go 1.18 is released, use [`gotip`](https://pkg.go.dev/golang.org/dl/gotip). Assuming you've got a working kubeconfig (does `kubectl get pods` work?), you can run this code: diff --git a/generic/client.go b/generic/client.go new file mode 100644 index 0000000..57d3b12 --- /dev/null +++ b/generic/client.go @@ -0,0 +1,83 @@ +package generic + +import ( + "bytes" + "context" + "encoding/json" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + + +type obj interface { + GetObjectKind() schema.ObjectKind +} + +func NewClient[T obj](config *rest.Config) client[T] { + return client[T] { + dyn: dynamic.NewForConfigOrDie(config), + } +} + +type client[T obj] struct { + dyn dynamic.Interface // TODO: don't depend on dynamic client +} + +// gvr translates the T to a GVR, via GVK +// GVK->GVR translation is a terrible hack; we need something better. +func (c client[T]) gvr() schema.GroupVersionResource { + // BOOM! + // This panics because T is a nil *corev1.Pod; the runtime.Object interface is only satisfied by *corev1.Pod, not the instantiable corev1.Pod. + // https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example + var t T + gvk := t.GetObjectKind().GroupVersionKind() + return schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: strings.ToLower(gvk.Kind) + "s", // HACK + } +} + +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 +} + diff --git a/main.go b/main.go index f9e67a5..8c79b21 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,22 @@ package main import ( - "bytes" - "encoding/json" "context" + "encoding/json" "log" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" + "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() { + // This is load bearing... + // Without this, while the client is in generic/, I get a mysterious compiler error: + // "internal compiler error: missing import reader for json.NewEncoder" + _ = json.NewEncoder(nil) + ctx := context.Background() config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig() if err != nil { @@ -24,16 +24,18 @@ func main() { } // List pods in kube-system. - pods, err := NewClient[pod](config).List(ctx, "kube-system") - if err != nil { log.Fatal("listing pods:", err) } + pods, err := generic.NewClient[*corev1.Pod](config).List(ctx, "kube-system") + if err != nil { + log.Fatal("listing pods:", err) + } log.Println("PODS") for _, p := range pods { log.Println("-", p.Name) } // Create a ConfigMap, then list ConfigMaps. - cmc := NewClient[cm](config) - if err := cmc.Create(ctx, "kube-system",cm{ + cmc := generic.NewClient[*corev1.ConfigMap](config) + if err := cmc.Create(ctx, "kube-system", &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "foo-", }, @@ -44,96 +46,11 @@ func main() { log.Fatal("creating configmap:", err) } cms, err := cmc.List(ctx, "kube-system") - if err != nil { log.Fatal("listing configmaps:", err) } + if err != nil { + log.Fatal("listing configmaps:", err) + } log.Println("CONFIGMAPS") for _, cm := range cms { log.Println("-", cm.Name) } } - -func NewClient[T obj](config *rest.Config) client[T] { - return client[T] { - dyn: dynamic.NewForConfigOrDie(config), - } -} - -type client[T obj] struct { - dyn dynamic.Interface -} - -func (c client[T]) List(ctx context.Context, namespace string) ([]T, error) { - var t T - ul, err := c.dyn.Resource(t.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(t.GVR()).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - 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 } - 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(t.GVR()).Namespace(namespace).Create(ctx, u, metav1.CreateOptions{}) - return err -} - -// THESE ARE HACKS -// -// Ideally, the Pod type (or any runtime.Object) would be able to give us its -// GVR directly, then the obj interface / type constraint would just be -// runtime.Object. -// -// Since it can't, we have to wrap runtime.Object and provide these per-type. -// This would be really nice to fix. - -type pod corev1.Pod -func (pod pod) GetObjectKind() schema.ObjectKind { return pod.GetObjectKind() } -func (pod pod) DeepCopyObject() runtime.Object { return pod.DeepCopyObject() } -func (pod) GVR() schema.GroupVersionResource { - return schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "pods", - } -} - -type cm corev1.ConfigMap -func (cm cm) GetObjectKind() schema.ObjectKind { return cm.GetObjectKind() } -func (cm cm) DeepCopyObject() runtime.Object { return cm.DeepCopyObject() } -func (cm) GVR() schema.GroupVersionResource { - return schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "configmaps", - } -} - -type obj interface { - runtime.Object - - GVR() schema.GroupVersionResource -} -