~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
291 lines · 6.9 KB Raw History
package kube

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/kcprest"
)

type Doc = map[string]any

type Client struct {
	dyn dynamic.Interface
}

func New(cfg *rest.Config) (*Client, error) {
	if cfg == nil {
		return nil, errors.New("kube: a rest config is required")
	}
	dyn, err := dynamic.NewForConfig(cfg)
	if err != nil {
		return nil, fmt.Errorf("kube: the client for %s: %w", cfg.Host, err)
	}
	return &Client{dyn: dyn}, nil
}

func ForWorkspace(base *rest.Config, workspace string) (*Client, error) {
	if base == nil {
		return nil, errors.New("kube: a rest config is required")
	}
	return New(kcprest.ForCluster(base, workspace))
}

type Factory func(workspace string) (*Client, error)

func NewFactory(base *rest.Config) Factory {
	return func(workspace string) (*Client, error) { return ForWorkspace(base, workspace) }
}

type Resource struct {
	ri  dynamic.NamespaceableResourceInterface
	gvr schema.GroupVersionResource
	ns  string
}

func (c *Client) Resource(gvr schema.GroupVersionResource) Resource {
	if c == nil || c.dyn == nil {
		return Resource{gvr: gvr}
	}
	return Resource{ri: c.dyn.Resource(gvr), gvr: gvr}
}

func (r Resource) In(namespace string) Resource {
	r.ns = namespace
	return r
}

func (r Resource) GVR() schema.GroupVersionResource { return r.gvr }

func (r Resource) Namespace() string { return r.ns }

func (r Resource) iface() (dynamic.ResourceInterface, error) {
	if r.ri == nil {
		return nil, fmt.Errorf("kube: %s has no client", r.gvr.Resource)
	}
	if r.ns != "" {
		return r.ri.Namespace(r.ns), nil
	}
	return r.ri, nil
}

type ApplyOptions struct {
	FieldManager string

	Force bool

	Status bool

	DryRun []string
}

func Get[T any](ctx context.Context, r Resource, name string) (*T, error) {
	ri, err := r.iface()
	if err != nil {
		return nil, err
	}
	u, err := ri.Get(ctx, name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	return Decode[T](u.Object)
}

func List[T any](ctx context.Context, r Resource, opts metav1.ListOptions) ([]T, error) {
	ri, err := r.iface()
	if err != nil {
		return nil, err
	}
	l, err := ri.List(ctx, opts)
	if err != nil {
		return nil, err
	}
	out := make([]T, 0, len(l.Items))
	for i := range l.Items {
		v, err := Decode[T](l.Items[i].Object)
		if err != nil {
			return nil, err
		}
		out = append(out, *v)
	}
	return out, nil
}

func Create[T any](ctx context.Context, r Resource, obj any, opts metav1.CreateOptions) (*T, error) {
	ri, u, err := r.prepare(obj)
	if err != nil {
		return nil, err
	}
	got, err := ri.Create(ctx, u, opts)
	if err != nil {
		return nil, err
	}
	return Decode[T](got.Object)
}

func Update[T any](ctx context.Context, r Resource, obj any, opts metav1.UpdateOptions) (*T, error) {
	ri, u, err := r.prepare(obj)
	if err != nil {
		return nil, err
	}
	got, err := ri.Update(ctx, u, opts)
	if err != nil {
		return nil, err
	}
	return Decode[T](got.Object)
}

func UpdateStatus[T any](ctx context.Context, r Resource, obj any, opts metav1.UpdateOptions) (*T, error) {
	ri, u, err := r.prepare(obj)
	if err != nil {
		return nil, err
	}
	got, err := ri.UpdateStatus(ctx, u, opts)
	if err != nil {
		return nil, err
	}
	return Decode[T](got.Object)
}

func Apply[T any](ctx context.Context, r Resource, obj any, o ApplyOptions) (*T, error) {
	ri, err := r.iface()
	if err != nil {
		return nil, err
	}
	doc, err := ApplyBody(obj, o.Status)
	if err != nil {
		return nil, err
	}
	name, _, _ := unstructured.NestedString(doc, "metadata", "name")
	if strings.TrimSpace(name) == "" {
		return nil, fmt.Errorf("kube: applying %s: the object has no name", r.gvr.Resource)
	}
	body, err := json.Marshal(doc)
	if err != nil {
		return nil, fmt.Errorf("kube: encoding %s %s: %w", r.gvr.Resource, name, err)
	}
	opts := metav1.PatchOptions{FieldManager: o.FieldManager, DryRun: o.DryRun}
	if o.Force {
		opts.Force = new(true)
	}
	var sub []string
	if o.Status {
		sub = []string{"status"}
	}
	got, err := ri.Patch(ctx, name, types.ApplyPatchType, body, opts, sub...)
	if err != nil {
		return nil, err
	}
	return Decode[T](got.Object)
}

func Patch[T any](ctx context.Context, r Resource, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*T, error) {
	ri, err := r.iface()
	if err != nil {
		return nil, err
	}
	got, err := ri.Patch(ctx, name, pt, data, opts, subresources...)
	if err != nil {
		return nil, err
	}
	return Decode[T](got.Object)
}

func (r Resource) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	ri, err := r.iface()
	if err != nil {
		return err
	}
	return ri.Delete(ctx, name, opts)
}

func (r Resource) prepare(obj any) (dynamic.ResourceInterface, *unstructured.Unstructured, error) {
	ri, err := r.iface()
	if err != nil {
		return nil, nil, err
	}
	doc, err := Encode(obj)
	if err != nil {
		return nil, nil, err
	}
	return ri, &unstructured.Unstructured{Object: doc}, nil
}

func ApplyBody(obj any, status bool) (Doc, error) {
	if d, ok := asDoc(obj); ok {
		av, _ := d["apiVersion"].(string)
		kind, _ := d["kind"].(string)
		if av == "" || kind == "" {
			return nil, errors.New("kube: an applied document needs apiVersion and kind")
		}
		return d, nil
	}
	full, err := Encode(obj)
	if err != nil {
		return nil, err
	}
	meta, _ := full["metadata"].(map[string]any)
	out := Doc{"apiVersion": full["apiVersion"], "kind": full["kind"]}
	skeleton := map[string]any{}
	keys := []string{"name", "namespace"}
	if !status {
		keys = append(keys, "labels", "annotations", "ownerReferences", "finalizers")
	}
	for _, k := range keys {
		if v, ok := meta[k]; ok && v != nil {
			skeleton[k] = v
		}
	}
	out["metadata"] = skeleton
	if status {
		st, _ := full["status"].(map[string]any)
		if st == nil {
			st = map[string]any{}
		}
		out["status"] = st
		return out, nil
	}
	for k, v := range full {
		if k != "apiVersion" && k != "kind" && k != "metadata" && k != "status" {
			out[k] = v
		}
	}
	return out, nil
}

func ApplyWith(ctx context.Context, cl client.Client, obj any, o ApplyOptions) error {
	if cl == nil {
		return errors.New("kube: applying needs a client")
	}
	doc, err := ApplyBody(obj, o.Status)
	if err != nil {
		return err
	}
	ac := client.ApplyConfigurationFromUnstructured(&unstructured.Unstructured{Object: doc})
	if o.Status {
		opts := []client.SubResourceApplyOption{client.FieldOwner(o.FieldManager)}
		if o.Force {
			opts = append(opts, client.ForceOwnership)
		}
		return cl.Status().Apply(ctx, ac, opts...)
	}
	opts := []client.ApplyOption{client.FieldOwner(o.FieldManager)}
	if o.Force {
		opts = append(opts, client.ForceOwnership)
	}
	if len(o.DryRun) > 0 {
		opts = append(opts, client.DryRunAll)
	}
	return cl.Apply(ctx, ac, opts...)
}