// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 //nolint:dupl package client import ( "context" "encoding/json" "fmt" "strings" "gomodules.xyz/jsonpatch/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/projectcapsule/capsule/pkg/api/meta" ) type JSONPatch struct { Operation JSONPatchOperation `json:"op"` Path string `json:"path"` Value any `json:"value,omitempty"` } type JSONPatchOperation string const ( JSONPatchAdd JSONPatchOperation = "add" JSONPatchReplace JSONPatchOperation = "replace" JSONPatchRemove JSONPatchOperation = "remove" ) func (j JSONPatchOperation) String() string { return string(j) } func EscapeJSONPointer(s string) string { s = strings.ReplaceAll(s, "~", "~0") s = strings.ReplaceAll(s, "/", "~1") return s } func JSONPatchesToRawPatch(patches []JSONPatch) (patch []byte, err error) { return json.Marshal(patches) } func JSONPatchesToJSONPatchOperation( patches []JSONPatch, ) []jsonpatch.JsonPatchOperation { if len(patches) == 0 { return nil } ops := make([]jsonpatch.JsonPatchOperation, 0, len(patches)) for _, p := range patches { ops = append(ops, jsonpatch.JsonPatchOperation{ Operation: p.Operation.String(), Path: p.Path, Value: p.Value, }) } return ops } func ApplyPatches( ctx context.Context, c client.Client, obj client.Object, patches []JSONPatch, manager string, ) (err error) { if len(patches) == 0 { return nil } rawPatch, err := JSONPatchesToRawPatch(patches) if err != nil { return err } return c.Patch( ctx, obj, client.RawPatch(types.JSONPatchType, rawPatch), client.FieldOwner(manager), ) } func AddLabelsPatch(labels map[string]string, keys map[string]string) []JSONPatch { if len(keys) == 0 { return nil } patches := make([]JSONPatch, 0, len(keys)+1) // If labels is nil, /metadata/labels likely doesn't exist. // JSONPatch add/replace to /metadata/labels/ requires /metadata/labels to exist. if labels == nil { patches = append(patches, JSONPatch{ Operation: JSONPatchAdd, Path: "/metadata/labels", Value: map[string]string{}, }) labels = map[string]string{} // local view for replace/add decision } for key, val := range keys { op := JSONPatchAdd if existing, ok := labels[key]; ok { if existing == val { continue } op = JSONPatchReplace } patches = append(patches, JSONPatch{ Operation: op, Path: fmt.Sprintf("/metadata/labels/%s", EscapeJSONPointer(key)), Value: val, }) } return patches } func AddAnnotationsPatch(annotations map[string]string, keys map[string]string) []JSONPatch { if len(keys) == 0 { return nil } patches := make([]JSONPatch, 0, len(keys)+1) // If annotations is nil, /metadata/annotations likely doesn't exist. // JSONPatch add/replace to /metadata/annotations/ requires /metadata/annotations to exist. if annotations == nil { patches = append(patches, JSONPatch{ Operation: JSONPatchAdd, Path: "/metadata/annotations", Value: map[string]string{}, }) annotations = map[string]string{} } for key, val := range keys { op := JSONPatchAdd if existing, ok := annotations[key]; ok { if existing == val { continue } op = JSONPatchReplace } patches = append(patches, JSONPatch{ Operation: op, Path: fmt.Sprintf("/metadata/annotations/%s", EscapeJSONPointer(key)), Value: val, }) } return patches } // PatchRemoveLabels returns a JSONPatch array for removing labels with matching keys. func PatchRemoveLabels(labels map[string]string, keys []string) []JSONPatch { var patches []JSONPatch if labels == nil { return patches } for _, key := range keys { if _, ok := labels[key]; ok { path := fmt.Sprintf("/metadata/labels/%s", EscapeJSONPointer(key)) patches = append(patches, JSONPatch{ Operation: JSONPatchRemove, Path: path, }) } } return patches } // PatchRemoveAnnotations returns a JSONPatch array for removing annotations with matching keys. func PatchRemoveAnnotations(annotations map[string]string, keys []string) []JSONPatch { var patches []JSONPatch if annotations == nil { return patches } for _, key := range keys { if _, ok := annotations[key]; ok { path := fmt.Sprintf("/metadata/annotations/%s", EscapeJSONPointer(key)) patches = append(patches, JSONPatch{ Operation: JSONPatchRemove, Path: path, }) } } return patches } func AddOwnerReferencePatch( ownerrefs []metav1.OwnerReference, ownerreference *metav1.OwnerReference, ) []JSONPatch { if ownerreference == nil { return nil } patches := make([]JSONPatch, 0, 2) // Ensure parent exists if missing (nil slice usually means field absent) if ownerrefs == nil { patches = append(patches, JSONPatch{ Operation: JSONPatchAdd, Path: "/metadata/ownerReferences", Value: []metav1.OwnerReference{}, }) patches = append(patches, JSONPatch{ Operation: JSONPatchAdd, Path: "/metadata/ownerReferences/-", Value: ownerreference, }) return patches } for i := range ownerrefs { if ownerrefs[i].UID != ownerreference.UID { continue } existing := ownerrefs[i] if meta.LooseOwnerReferenceEqual(existing, *ownerreference) { return nil } patches = append(patches, JSONPatch{ Operation: JSONPatchReplace, Path: fmt.Sprintf("/metadata/ownerReferences/%d", i), Value: ownerreference, }) return patches } // Otherwise append patches = append(patches, JSONPatch{ Operation: JSONPatchAdd, Path: "/metadata/ownerReferences/-", Value: ownerreference, }) return patches } func RemoveOwnerReferencePatch( ownerRefs []metav1.OwnerReference, toRemove *metav1.OwnerReference, ) []JSONPatch { if toRemove == nil { return nil } if len(ownerRefs) == 0 { return nil } idx := -1 for i := range ownerRefs { if meta.LooseOwnerReferenceEqual(ownerRefs[i], *toRemove) { idx = i break } } if idx == -1 { return nil } patches := []JSONPatch{ { Operation: JSONPatchRemove, Path: fmt.Sprintf("/metadata/ownerReferences/%d", idx), }, } if len(ownerRefs) == 1 { patches = append(patches, JSONPatch{ Operation: JSONPatchRemove, Path: "/metadata/ownerReferences", }) } return patches }