Files
velero/internal/resourcemodifiers/resource_modifiers.go
2023-09-29 04:25:01 +01:00

212 lines
6.1 KiB
Go

/*
Copyright The Velero Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcemodifiers
import (
"fmt"
"regexp"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/yaml"
"github.com/vmware-tanzu/velero/pkg/util/collections"
)
const (
ConfigmapRefType = "configmap"
ResourceModifierSupportedVersionV1 = "v1"
)
type JSONPatch struct {
Operation string `json:"operation"`
From string `json:"from,omitempty"`
Path string `json:"path"`
Value string `json:"value,omitempty"`
}
type Conditions struct {
Namespaces []string `json:"namespaces,omitempty"`
GroupResource string `json:"groupResource"`
ResourceNameRegex string `json:"resourceNameRegex,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
}
type ResourceModifierRule struct {
Conditions Conditions `json:"conditions"`
Patches []JSONPatch `json:"patches"`
}
type ResourceModifiers struct {
Version string `json:"version"`
ResourceModifierRules []ResourceModifierRule `json:"resourceModifierRules"`
}
func GetResourceModifiersFromConfig(cm *v1.ConfigMap) (*ResourceModifiers, error) {
if cm == nil {
return nil, fmt.Errorf("could not parse config from nil configmap")
}
if len(cm.Data) != 1 {
return nil, fmt.Errorf("illegal resource modifiers %s/%s configmap", cm.Namespace, cm.Name)
}
var yamlData string
for _, v := range cm.Data {
yamlData = v
}
resModifiers, err := unmarshalResourceModifiers([]byte(yamlData))
if err != nil {
return nil, errors.WithStack(err)
}
return resModifiers, nil
}
func (p *ResourceModifiers) ApplyResourceModifierRules(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) []error {
var errs []error
for _, rule := range p.ResourceModifierRules {
err := rule.Apply(obj, groupResource, log)
if err != nil {
errs = append(errs, err)
}
}
return errs
}
func (r *ResourceModifierRule) Apply(obj *unstructured.Unstructured, groupResource string, log logrus.FieldLogger) error {
namespaceInclusion := collections.NewIncludesExcludes().Includes(r.Conditions.Namespaces...)
if !namespaceInclusion.ShouldInclude(obj.GetNamespace()) {
return nil
}
if r.Conditions.GroupResource != groupResource {
return nil
}
if r.Conditions.ResourceNameRegex != "" {
match, err := regexp.MatchString(r.Conditions.ResourceNameRegex, obj.GetName())
if err != nil {
return errors.Errorf("error in matching regex %s", err.Error())
}
if !match {
return nil
}
}
if r.Conditions.LabelSelector != nil {
selector, err := metav1.LabelSelectorAsSelector(r.Conditions.LabelSelector)
if err != nil {
return errors.Errorf("error in creating label selector %s", err.Error())
}
if !selector.Matches(labels.Set(obj.GetLabels())) {
return nil
}
}
patches, err := r.PatchArrayToByteArray()
if err != nil {
return err
}
log.Infof("Applying resource modifier patch on %s/%s", obj.GetNamespace(), obj.GetName())
err = ApplyPatch(patches, obj, log)
if err != nil {
return err
}
return nil
}
// PatchArrayToByteArray converts all JsonPatch to string array with the format of jsonpatch.Patch and then convert it to byte array
func (r *ResourceModifierRule) PatchArrayToByteArray() ([]byte, error) {
var patches []string
for _, patch := range r.Patches {
patches = append(patches, patch.ToString())
}
patchesStr := strings.Join(patches, ",\n\t")
return []byte(fmt.Sprintf(`[%s]`, patchesStr)), nil
}
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 ApplyPatch(patch []byte, obj *unstructured.Unstructured, log logrus.FieldLogger) error {
jsonPatch, err := jsonpatch.DecodePatch(patch)
if err != nil {
return fmt.Errorf("error in decoding json patch %s", err.Error())
}
objBytes, err := obj.MarshalJSON()
if err != nil {
return fmt.Errorf("error in marshaling object %s", err.Error())
}
modifiedObjBytes, err := jsonPatch.Apply(objBytes)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
log.Infof("Test operation failed for JSON Patch %s", err.Error())
return nil
}
return fmt.Errorf("error in applying JSON Patch %s", err.Error())
}
err = obj.UnmarshalJSON(modifiedObjBytes)
if err != nil {
return fmt.Errorf("error in unmarshalling modified object %s", err.Error())
}
return nil
}
func unmarshalResourceModifiers(yamlData []byte) (*ResourceModifiers, error) {
resModifiers := &ResourceModifiers{}
err := yaml.UnmarshalStrict(yamlData, resModifiers)
if err != nil {
return nil, fmt.Errorf("failed to decode yaml data into resource modifiers %v", err)
}
return resModifiers, nil
}
func addQuotes(value string) bool {
if 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 _, err := strconv.ParseBool(value); err == nil {
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
}