Skip to content

Commit

Permalink
PB-4580:
Browse files Browse the repository at this point in the history
   - Change TransformResources resource collector code to accept the array index in the path
  • Loading branch information
sgajawada-px committed Oct 13, 2023
1 parent aef5257 commit aeea9b4
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 6 deletions.
6 changes: 6 additions & 0 deletions pkg/migration/controllers/resourcetransformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"reflect"
"regexp"
"strings"

"github.com/libopenstorage/stork/drivers/volume"
Expand All @@ -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"
Expand Down Expand Up @@ -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 ")
Expand Down
232 changes: 226 additions & 6 deletions pkg/resourcecollector/resourcetransformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package resourcecollector

import (
"fmt"
"regexp"
"strings"

stork_api "github.com/libopenstorage/stork/pkg/apis/stork/v1alpha1"
Expand Down Expand Up @@ -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
Expand All @@ -69,21 +70,21 @@ 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
}
}

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)
}
Expand All @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}

0 comments on commit aeea9b4

Please sign in to comment.