From aeea9b471287d6272821e5eec4ba582cca8503e2 Mon Sep 17 00:00:00 2001 From: Santosh Kumar Gajawada Date: Fri, 13 Oct 2023 20:29:31 +0530 Subject: [PATCH] PB-4580: - Change TransformResources resource collector code to accept the array index in the path --- .../controllers/resourcetransformation.go | 6 + .../resourcetransformation.go | 232 +++++++++++++++++- 2 files changed, 232 insertions(+), 6 deletions(-) diff --git a/pkg/migration/controllers/resourcetransformation.go b/pkg/migration/controllers/resourcetransformation.go index a68655be36..40f7615f4b 100644 --- a/pkg/migration/controllers/resourcetransformation.go +++ b/pkg/migration/controllers/resourcetransformation.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "regexp" "strings" "github.com/libopenstorage/stork/drivers/volume" @@ -30,6 +31,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +var pathRegexp = regexp.MustCompile(`^([a-zA-Z_/][a-zA-Z0-9_/]*(\[[0-9]+\])?\.)*[a-zA-Z_/][a-zA-Z0-9_/]*$`) + const ( // ResourceTransformationControllerName of resource transformation CR handler ResourceTransformationControllerName = "resource-transformation-controller" @@ -173,6 +176,9 @@ func (r *ResourceTransformationController) validateSpecPath(transform *stork_api path.Type == stork_api.KeyPairResourceType) { return fmt.Errorf("unsupported type for resource %s, path %s, type: %s", kind, path.Path, path.Type) } + if !pathRegexp.MatchString(path.Path) { + return fmt.Errorf("invalid path for resource %s, path %s, type: %s", kind, path.Path, path.Type) + } } } log.TransformLog(transform).Infof("validated paths ") diff --git a/pkg/resourcecollector/resourcetransformation.go b/pkg/resourcecollector/resourcetransformation.go index 5b093fec89..fd6dd2f53c 100644 --- a/pkg/resourcecollector/resourcetransformation.go +++ b/pkg/resourcecollector/resourcetransformation.go @@ -2,6 +2,7 @@ package resourcecollector import ( "fmt" + "regexp" "strings" stork_api "github.com/libopenstorage/stork/pkg/apis/stork/v1alpha1" @@ -57,7 +58,7 @@ func TransformResources( value := getNewValueForPath(path.Value, string(path.Type)) if path.Type == stork_api.KeyPairResourceType { updateMap := value.(map[string]string) - err := unstructured.SetNestedStringMap(content, updateMap, strings.Split(path.Path, ".")...) + err := SetNestedStringMap(content, updateMap, path.Path) if err != nil { logrus.Errorf("Unable to apply patch path %s on resource kind: %s/,%s/%s, err: %v", path, patch.Kind, patch.Namespace, patch.Name, err) return err @@ -69,7 +70,7 @@ func TransformResources( return err } } else { - err := unstructured.SetNestedField(content, value, strings.Split(path.Path, ".")...) + err := SetNestedField(content, value, path.Path) if err != nil { logrus.Errorf("Unable to perform operation %s on path %s on resource kind: %s/,%s/%s, err: %v", path.Operation, path, patch.Kind, patch.Namespace, patch.Name, err) return err @@ -77,13 +78,13 @@ func TransformResources( } case stork_api.DeleteResourcePath: - unstructured.RemoveNestedField(content, strings.Split(path.Path, ".")...) + RemoveNestedField(content, strings.Split(path.Path, ".")...) logrus.Debugf("Removed patch path %s on resource kind: %s/,%s/%s", path, patch.Kind, patch.Namespace, patch.Name) case stork_api.ModifyResourcePathValue: var value interface{} if path.Type == stork_api.KeyPairResourceType { - currMap, _, err := unstructured.NestedMap(content, strings.Split(path.Path, ".")...) + currMap, _, err := NestedMap(content, strings.Split(path.Path, ".")...) if err != nil || len(currMap) == 0 { return fmt.Errorf("unable to find spec path, err: %v", err) } @@ -97,7 +98,7 @@ func TransformResources( } value = currMap } else if path.Type == stork_api.SliceResourceType { - currList, _, err := unstructured.NestedSlice(content, strings.Split(path.Path, ".")...) + currList, _, err := NestedSlice(content, strings.Split(path.Path, ".")...) if err != nil { return fmt.Errorf("unable to find spec path, err: %v", err) } @@ -109,7 +110,7 @@ func TransformResources( } else { value = path.Value } - err := unstructured.SetNestedField(content, value, strings.Split(path.Path, ".")...) + err := SetNestedField(content, value, path.Path) if err != nil { logrus.Errorf("Unable to perform operation %s on path %s on resource kind: %s/,%s/%s, err: %v", path.Operation, path, patch.Kind, patch.Namespace, patch.Name, err) return err @@ -157,3 +158,222 @@ func getNewValueForPath(oldVal, valType string) interface{} { } return updatedValue } + +var pathRegexpWithanArray = regexp.MustCompile(`^.+\[[0-9]+\](\.[a-zA-Z_/][a-zA-Z0-9_/]*)+$`) + +func jsonPath(fields []string) string { + return "." + strings.Join(fields, ".") +} + +// NestedSlice is wrapper around unstructured.NestedSlice function +// if the path doesn't consists of an index the call is transferred to unstructured.NestedSlice +// else it uses the same logic but includes changes to support the array index +func NestedSlice(obj map[string]interface{}, fields ...string) ([]interface{}, bool, error) { + if !pathRegexpWithanArray.MatchString(strings.Join(fields, ".")) { + return unstructured.NestedSlice(obj, fields...) + } + + val, found, err := NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return nil, found, err + } + _, ok := val.([]interface{}) + if !ok { + return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected []interface{}", jsonPath(fields), val, val) + } + return runtime.DeepCopyJSONValue(val).([]interface{}), true, nil +} + +// NestedMap is wrapper around unstructured.NestedMap function +// if the path doesn't consists of an index the call is transferred to unstructured.NestedMap +// else it uses the same logic but includes changes to support the array index +func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) { + if !pathRegexpWithanArray.MatchString(strings.Join(fields, ".")) { + return unstructured.NestedMap(obj, fields...) + } + + m, found, err := nestedMapNoCopy(obj, fields...) + if !found || err != nil { + return nil, found, err + } + return runtime.DeepCopyJSON(m), true, nil +} + +func nestedMapNoCopy(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) { + val, found, err := NestedFieldNoCopy(obj, fields...) + if !found || err != nil { + return nil, found, err + } + m, ok := val.(map[string]interface{}) + if !ok { + return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected map[string]interface{}", jsonPath(fields), val, val) + } + return m, true, nil +} + +func NestedFieldNoCopy(obj map[string]interface{}, fields ...string) (interface{}, bool, error) { + var val interface{} = obj + + for i, field := range fields { + if val == nil { + return nil, false, nil + } + if m, ok := val.(map[string]interface{}); ok { + var err error + val, ok, err = getValueFromMapKey(m, field) + if !ok || err != nil { + return nil, false, err + } + } else { + return nil, false, fmt.Errorf("%v accessor error: %v is of the type %T, expected map[string]interface{}", jsonPath(fields[:i+1]), val, val) + } + } + return val, true, nil +} + +// SetNestedStringSlice is wrapper around unstructured.SetNestedStringSlice function +// if the path doesn't consists of an index the call is transferred to unstructured.SetNestedStringSlice +// else it uses the same logic but includes changes to support the array index +func SetNestedStringSlice(obj map[string]interface{}, value []string, path string) error { + if !pathRegexpWithanArray.MatchString(path) { + return unstructured.SetNestedStringSlice(obj, value, strings.Split(path, ".")...) + } + + m := make([]interface{}, 0, len(value)) // convert []string into []interface{} + for _, v := range value { + m = append(m, v) + } + return setNestedFieldNoCopy(obj, m, strings.Split(path, ".")...) +} + +// SetNestedStringMap is wrapper around unstructured.SetNestedStringMap function +// if the path doesn't consists of an index the call is transferred to unstructured.SetNestedStringMap +// else it uses the same logic but includes changes to support the array index +func SetNestedStringMap(obj map[string]interface{}, value map[string]string, path string) error { + if !pathRegexpWithanArray.MatchString(path) { + return unstructured.SetNestedStringMap(obj, value, strings.Split(path, ".")...) + } + m := make(map[string]interface{}, len(value)) // convert map[string]string into map[string]interface{} + for k, v := range value { + m[k] = v + } + return setNestedFieldNoCopy(obj, m, strings.Split(path, ".")...) +} + +// SetNestedField is wrapper around unstructured.SetNestedField function +// if the path doesn't consists of an index the call is transferred to unstructured.SetNestedField +// else it uses the same logic but includes changes to support the array index +func SetNestedField(obj map[string]interface{}, value interface{}, path string) error { + if !pathRegexpWithanArray.MatchString(path) { + return unstructured.SetNestedField(obj, value, strings.Split(path, ".")...) + } + return setNestedFieldNoCopy(obj, runtime.DeepCopyJSONValue(value), strings.Split(path, ".")...) +} + +func setNestedFieldNoCopy(obj map[string]interface{}, value interface{}, fields ...string) error { + m := obj + + for index, field := range fields[:len(fields)-1] { + if val, ok, err := getValueFromMapKey(m, field); err != nil { + return err + } else if ok { + if valMap, ok := val.(map[string]interface{}); ok { + m = valMap + } else { + return fmt.Errorf("value cannot be set because %v is not a map[string]interface{}", jsonPath(fields[:index+1])) + } + } else { + newVal := make(map[string]interface{}) + if err := setMapKeyWithValue(m, newVal, field); err != nil { + return err + } + m = newVal + } + } + m[fields[len(fields)-1]] = value + return nil +} + +// RemoveNestedField is wrapper around unstructured.RemoveNestedField function +// if the path doesn't consists of an index the call is transferred to unstructured.RemoveNestedField +// else it uses the same logic but includes changes to support the array index +func RemoveNestedField(obj map[string]interface{}, fields ...string) error { + if !pathRegexpWithanArray.MatchString(strings.Join(fields, ".")) { + unstructured.RemoveNestedField(obj, fields...) + return nil + } + m := obj + for _, field := range fields[:len(fields)-1] { + if val, ok, err := getValueFromMapKey(m, field); err != nil { + return err + } else if ok { + if valMap, ok := val.(map[string]interface{}); ok { + m = valMap + } else { + return nil + } + } else { + return nil + } + } + delete(m, fields[len(fields)-1]) + return nil +} + +var indexDelimeter = func(c rune) bool { + return c == '[' || c == ']' +} + +// setMapKeyWithValue is to assign the value to the map m with key field, value newVal. here the field may even contain the array index +func setMapKeyWithValue(m, newVal map[string]interface{}, field string) error { + // check if an array index exists in the field + parts := strings.FieldsFunc(field, indexDelimeter) + if len(parts) != 2 { + m[field] = newVal + return nil + } + + // if the parts[0] is not an array send an error + arr := m[parts[0]] + value, ok := arr.([]interface{}) + if !ok { + return fmt.Errorf("value cannot be set because %v is not a []interface{}", arr) + } + + // append the newVal to the existing array + value = append(value, newVal) + m[parts[0]] = value + return nil +} + +// getValueFromMapKey is to retrive the value for the map m with key field. here the field may even contain the array index +func getValueFromMapKey(m map[string]interface{}, field string) (interface{}, bool, error) { + // check if an array index exists in the field + parts := strings.FieldsFunc(field, indexDelimeter) + if len(parts) != 2 { + value, ok := m[field] + return value, ok, nil + } + + // if the parts[0] is not an array send an error + arr := m[parts[0]] + value, ok := arr.([]interface{}) + if !ok { + return nil, false, fmt.Errorf("value cannot be set because %v is not a []interface{}", arr) + } + + // Convert the array index to int + var arrIndex int + _, err := fmt.Sscanf(parts[1], "%d", &arrIndex) + if err != nil { + return nil, false, err + } + + // send the approriate array object + if arrIndex < len(value) { + return value[arrIndex], true, nil + } else if arrIndex > len(value) { + return nil, false, fmt.Errorf("value cannot be set because index %d is out of range in array %v with length %d", arrIndex, arr, len(value)) + } + return nil, false, nil +}