mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-03-03 02:00:18 +00:00
313 lines
6.3 KiB
Go
313 lines
6.3 KiB
Go
// 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/<k> 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/<k> 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
|
|
}
|