package runtime import ( "context" "encoding/json" "errors" "fmt" "slices" "strings" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/strs" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" kruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" ) const finalizerManagerSuffix = "/finalizer" func (r Request) FieldManager() string { if r.scope == nil { return "" } return r.scope.fieldManager } func (r Request) Get(ctx context.Context, namespace, name string, obj Object) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } return r.scope.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, obj) } func (r Request) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } return r.scope.client.List(ctx, list, opts...) } func (r Request) ApplyStatus(ctx context.Context, obj Object) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } full, err := r.unstructured(obj) if err != nil { return err } u := r.skeleton(full, obj) status, _ := full.Object["status"].(map[string]any) if status == nil { status = map[string]any{} } if !r.scope.sharedStatus { if live, owned, err := r.liveStatus(ctx, obj); err == nil { if gone := dropped(live, status, owned, r.scope.foreignStatus, ""); len(gone) > 0 { if err := r.clearStatus(ctx, obj, gone); err != nil { return err } } } } u.Object["status"] = status if err := r.scope.client.Status().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner(r.scope.fieldManager), client.ForceOwnership); err != nil { if apierrors.IsMethodNotSupported(err) || apierrors.IsNotFound(err) { return nil } return fmt.Errorf("runtime: applying the status of %s: %w", obj.GetName(), err) } return nil } func (r Request) ApplyStatusAs(ctx context.Context, manager string, obj Object, status map[string]any) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } if manager == "" || manager == r.scope.fieldManager { return fmt.Errorf("runtime: applying a status subfield of %s needs a field manager of its own, not %q", obj.GetName(), manager) } full, err := r.unstructured(obj) if err != nil { return err } u := r.skeleton(full, obj) u.Object["status"] = status if err := r.scope.client.Status().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner(manager), client.ForceOwnership); err != nil { if apierrors.IsNotFound(err) { return nil } return fmt.Errorf("runtime: applying the status of %s as %s: %w", obj.GetName(), manager, err) } return nil } func (r Request) Apply(ctx context.Context, obj Object) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } full, err := r.unstructured(obj) if err != nil { return err } u := r.skeleton(full, obj) for k, v := range full.Object { if k != "apiVersion" && k != "kind" && k != "metadata" && k != "status" { u.Object[k] = v } } meta, _ := u.Object["metadata"].(map[string]any) if labels := obj.GetLabels(); len(labels) > 0 { meta["labels"] = strs.AnyMap(labels) } if annotations := obj.GetAnnotations(); len(annotations) > 0 { meta["annotations"] = strs.AnyMap(annotations) } if owners, ok, _ := unstructured.NestedSlice(full.Object, "metadata", "ownerReferences"); ok && len(owners) > 0 { meta["ownerReferences"] = owners } if err := r.scope.client.Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner(r.scope.fieldManager), client.ForceOwnership); err != nil { return fmt.Errorf("runtime: applying %s: %w", obj.GetName(), err) } return nil } func (r Request) ApplyFieldsAs(ctx context.Context, manager string, obj Object, fields map[string]any) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } if manager == "" || manager == r.scope.fieldManager { return fmt.Errorf("runtime: applying fields of %s needs a field manager of its own, not %q", obj.GetName(), manager) } full, err := r.unstructured(obj) if err != nil { return err } u := r.skeleton(full, obj) for k, v := range fields { if k != "apiVersion" && k != "kind" && k != "metadata" && k != "status" { u.Object[k] = v } } if err := r.scope.client.Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner(manager), client.ForceOwnership); err != nil { return fmt.Errorf("runtime: applying fields of %s as %s: %w", obj.GetName(), manager, err) } return nil } func (r Request) Delete(ctx context.Context, obj Object) error { if r.scope == nil || r.scope.client == nil { return errors.New("runtime: the request has no cluster client") } if err := r.scope.client.Delete(ctx, obj, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !apierrors.IsNotFound(err) { return fmt.Errorf("runtime: deleting %s: %w", obj.GetName(), err) } return nil } func (r Request) applyFinalizer(ctx context.Context, obj Object, present bool) error { if !present { return r.releaseFinalizer(ctx, obj) } full, err := r.unstructured(obj) if err != nil { return err } u := r.skeleton(full, obj) meta, _ := u.Object["metadata"].(map[string]any) meta["finalizers"] = []any{r.scope.finalizer} if err := r.scope.client.Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner(r.scope.fieldManager+finalizerManagerSuffix), client.ForceOwnership); err != nil { return fmt.Errorf("runtime: applying the finalizer of %s: %w", obj.GetName(), err) } return nil } func (r Request) releaseFinalizer(ctx context.Context, obj Object) error { current := obj.GetFinalizers() remaining := slices.DeleteFunc(slices.Clone(current), func(f string) bool { return f == r.scope.finalizer }) if len(remaining) == len(current) { return nil } body, err := json.Marshal([]map[string]any{ {"op": "test", "path": "/metadata/finalizers", "value": current}, {"op": "replace", "path": "/metadata/finalizers", "value": remaining}, }) if err != nil { return fmt.Errorf("runtime: encoding the finalizer release of %s: %w", obj.GetName(), err) } if err := r.scope.client.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, body)); err != nil { if apierrors.IsNotFound(err) { return nil } return fmt.Errorf("runtime: releasing the finalizer of %s: %w", obj.GetName(), err) } return nil } func (r Request) unstructured(obj Object) (*unstructured.Unstructured, error) { gvk, err := apiutil.GVKForObject(obj, r.scope.scheme) if err != nil { return nil, fmt.Errorf("runtime: the kind of %T: %w", obj, err) } m, err := kruntime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, fmt.Errorf("runtime: encoding %s: %w", obj.GetName(), err) } u := &unstructured.Unstructured{Object: m} u.SetGroupVersionKind(gvk) return u, nil } func (r Request) skeleton(full *unstructured.Unstructured, obj Object) *unstructured.Unstructured { meta := map[string]any{"name": obj.GetName()} if ns := obj.GetNamespace(); ns != "" { meta["namespace"] = ns } return &unstructured.Unstructured{Object: map[string]any{ "apiVersion": full.GetAPIVersion(), "kind": full.GetKind(), "metadata": meta, }} } func (r Request) liveStatus(ctx context.Context, obj Object) (map[string]any, map[string]any, error) { gvk, err := apiutil.GVKForObject(obj, r.scope.scheme) if err != nil { return nil, nil, err } fresh, err := r.scope.scheme.New(gvk) if err != nil { return nil, nil, err } live, ok := fresh.(client.Object) if !ok { return nil, nil, fmt.Errorf("runtime: %T is not an object", fresh) } if err := r.scope.client.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, live); err != nil { return nil, nil, err } m, err := kruntime.DefaultUnstructuredConverter.ToUnstructured(live) if err != nil { return nil, nil, err } status, _ := m["status"].(map[string]any) return status, ownedStatus(live.GetManagedFields(), r.scope.fieldManager), nil } func ownedStatus(entries []metav1.ManagedFieldsEntry, manager string) map[string]any { owned := map[string]any{} for _, e := range entries { if e.Manager != manager || e.FieldsV1 == nil { continue } var fields map[string]any if json.Unmarshal(e.FieldsV1.Raw, &fields) != nil { continue } if node, ok := fields["f:status"].(map[string]any); ok { mergeFields(owned, node) } } return owned } func mergeFields(into, from map[string]any) { for k, v := range from { sub, isMap := v.(map[string]any) cur, has := into[k].(map[string]any) switch { case isMap && has: mergeFields(cur, sub) case isMap: next := map[string]any{} mergeFields(next, sub) into[k] = next default: into[k] = v } } } func (r Request) clearStatus(ctx context.Context, obj Object, gone map[string]any) error { body, err := json.Marshal(map[string]any{"status": gone}) if err != nil { return fmt.Errorf("runtime: encoding the dropped status fields of %s: %w", obj.GetName(), err) } target, ok := obj.DeepCopyObject().(Object) if !ok { return fmt.Errorf("runtime: %T is not an object", obj) } if err := r.scope.client.Status().Patch(ctx, target, client.RawPatch(types.MergePatchType, body), client.FieldOwner(r.scope.fieldManager)); err != nil { if apierrors.IsMethodNotSupported(err) || apierrors.IsNotFound(err) { return nil } return fmt.Errorf("runtime: clearing the dropped status fields of %s: %w", obj.GetName(), err) } return nil } func dropped(live, desired, owned map[string]any, foreign []string, prefix string) map[string]any { gone := map[string]any{} for k, v := range live { path := prefix + k if slices.Contains(foreign, path) { delete(desired, k) continue } mine, isMine := owned["f:"+k] if !isMine { continue } mineMap, _ := mine.(map[string]any) liveMap, liveIsMap := v.(map[string]any) next, ok := desired[k] if !ok || next == nil { delete(desired, k) if !liveIsMap || !holdsForeign(foreign, path) { gone[k] = nil continue } next = map[string]any{} } nextMap, nextIsMap := next.(map[string]any) if liveIsMap && nextIsMap { if sub := dropped(liveMap, nextMap, mineMap, foreign, path+"."); len(sub) > 0 { gone[k] = sub } } } return gone } func holdsForeign(foreign []string, path string) bool { for _, f := range foreign { if strings.HasPrefix(f, path+".") { return true } } return false }