velero/internal/resourcemodifiers/json_patch.go

104 lines
2.9 KiB
Go

package resourcemodifiers
import (
"errors"
"fmt"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type JSONPatch struct {
Operation string `json:"operation"`
From string `json:"from,omitempty"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
}
func (p *JSONPatch) ToString() string {
if addQuotes(&p.Value) {
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": "%s"}`, p.Operation, p.From, p.Path, p.Value)
}
return fmt.Sprintf(`{"op": "%s", "from": "%s", "path": "%s", "value": %s}`, p.Operation, p.From, p.Path, p.Value)
}
func addQuotes(value *string) bool {
if *value == "" {
return true
}
// if value is escaped, remove escape and add quotes
// this is useful for scenarios where boolean, null and numbers are required to be set as string.
if strings.HasPrefix(*value, "\"") && strings.HasSuffix(*value, "\"") {
*value = strings.TrimPrefix(*value, "\"")
*value = strings.TrimSuffix(*value, "\"")
return true
}
// if value is null, then don't add quotes
if *value == "null" {
return false
}
// if value is a boolean, then don't add quotes
if strings.ToLower(*value) == "true" || strings.ToLower(*value) == "false" {
return false
}
// if value is a json object or array, then don't add quotes.
if strings.HasPrefix(*value, "{") || strings.HasPrefix(*value, "[") {
return false
}
// if value is a number, then don't add quotes
if _, err := strconv.ParseFloat(*value, 64); err == nil {
return false
}
return true
}
type JSONPatcher struct {
patches []JSONPatch `yaml:"patches"`
}
func (p *JSONPatcher) Patch(u *unstructured.Unstructured, logger logrus.FieldLogger) (*unstructured.Unstructured, error) {
modifiedObjBytes, err := p.applyPatch(u)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
logger.Infof("Test operation failed for JSON Patch %s", err.Error())
return u.DeepCopy(), nil
}
return nil, fmt.Errorf("error in applying JSON Patch %s", err.Error())
}
updated := &unstructured.Unstructured{}
err = updated.UnmarshalJSON(modifiedObjBytes)
if err != nil {
return nil, fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return updated, nil
}
func (p *JSONPatcher) applyPatch(u *unstructured.Unstructured) ([]byte, error) {
patchBytes := p.patchArrayToByteArray()
jsonPatch, err := jsonpatch.DecodePatch(patchBytes)
if err != nil {
return nil, fmt.Errorf("error in decoding json patch %s", err.Error())
}
objBytes, err := u.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("error in marshaling object %s", err.Error())
}
return jsonPatch.Apply(objBytes)
}
func (p *JSONPatcher) patchArrayToByteArray() []byte {
var patches []string
for _, patch := range p.patches {
patches = append(patches, patch.ToString())
}
patchesStr := strings.Join(patches, ",\n\t")
return []byte(fmt.Sprintf(`[%s]`, patchesStr))
}