istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/tpath/tree.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  /*
    16  tree.go contains functions for traversing and updating a tree constructed from yaml or json.Unmarshal.
    17  Nodes in such trees have the form map[interface{}]interface{} or map[interface{}][]interface{}.
    18  For some tree updates, like delete or append, it's necessary to have access to the parent node. PathContext is a
    19  tree constructed during tree traversal that gives access to ancestor nodes all the way up to the root, which can be
    20  used for this purpose.
    21  */
    22  package tpath
    23  
    24  import (
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"reflect"
    29  	"regexp"
    30  	"strconv"
    31  	"strings"
    32  
    33  	"gopkg.in/yaml.v2"
    34  	yaml2 "sigs.k8s.io/yaml"
    35  
    36  	"istio.io/istio/operator/pkg/util"
    37  	"istio.io/istio/pkg/log"
    38  )
    39  
    40  var scope = log.RegisterScope("tpath", "tree traverser")
    41  
    42  // PathContext provides a means for traversing a tree towards the root.
    43  type PathContext struct {
    44  	// Parent in the Parent of this PathContext.
    45  	Parent *PathContext
    46  	// KeyToChild is the key required to reach the child.
    47  	KeyToChild any
    48  	// Node is the actual Node in the data tree.
    49  	Node any
    50  }
    51  
    52  // String implements the Stringer interface.
    53  func (nc *PathContext) String() string {
    54  	ret := "\n--------------- NodeContext ------------------\n"
    55  	if nc.Parent != nil {
    56  		ret += fmt.Sprintf("Parent.Node=\n%s\n", nc.Parent.Node)
    57  		ret += fmt.Sprintf("KeyToChild=%v\n", nc.Parent.KeyToChild)
    58  	}
    59  
    60  	ret += fmt.Sprintf("Node=\n%s\n", nc.Node)
    61  	ret += "----------------------------------------------\n"
    62  
    63  	return ret
    64  }
    65  
    66  // GetPathContext returns the PathContext for the Node which has the given path from root.
    67  // It returns false and no error if the given path is not found, or an error code in other error situations, like
    68  // a malformed path.
    69  // It also creates a tree of PathContexts during the traversal so that Parent nodes can be updated if required. This is
    70  // required when (say) appending to a list, where the parent list itself must be updated.
    71  func GetPathContext(root any, path util.Path, createMissing bool) (*PathContext, bool, error) {
    72  	return getPathContext(&PathContext{Node: root}, path, path, createMissing)
    73  }
    74  
    75  // WritePathContext writes the given value to the Node in the given PathContext.
    76  func WritePathContext(nc *PathContext, value any, merge bool) error {
    77  	scope.Debugf("WritePathContext PathContext=%s, value=%v", nc, value)
    78  
    79  	if !util.IsValueNil(value) {
    80  		return setPathContext(nc, value, merge)
    81  	}
    82  
    83  	scope.Debug("delete")
    84  	if nc.Parent == nil {
    85  		return errors.New("cannot delete root element")
    86  	}
    87  
    88  	switch {
    89  	case isSliceOrPtrInterface(nc.Parent.Node):
    90  		if err := util.DeleteFromSlicePtr(nc.Parent.Node, nc.Parent.KeyToChild.(int)); err != nil {
    91  			return err
    92  		}
    93  		if isMapOrInterface(nc.Parent.Parent.Node) {
    94  			return util.InsertIntoMap(nc.Parent.Parent.Node, nc.Parent.Parent.KeyToChild, nc.Parent.Node)
    95  		}
    96  		// TODO: The case of deleting a list.list.node element is not currently supported.
    97  		return fmt.Errorf("cannot delete path: unsupported parent.parent type %T for delete", nc.Parent.Parent.Node)
    98  	case util.IsMap(nc.Parent.Node):
    99  		return util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild)
   100  	default:
   101  	}
   102  	return fmt.Errorf("cannot delete path: unsupported parent type %T for delete", nc.Parent.Node)
   103  }
   104  
   105  // WriteNode writes value to the tree in root at the given path, creating any required missing internal nodes in path.
   106  func WriteNode(root any, path util.Path, value any) error {
   107  	pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	return WritePathContext(pc, value, false)
   112  }
   113  
   114  // MergeNode merges value to the tree in root at the given path, creating any required missing internal nodes in path.
   115  func MergeNode(root any, path util.Path, value any) error {
   116  	pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return WritePathContext(pc, value, true)
   121  }
   122  
   123  // Find returns the value at path from the given tree, or false if the path does not exist.
   124  // It behaves differently from GetPathContext in that it never creates map entries at the leaf and does not provide
   125  // a way to mutate the parent of the found node.
   126  func Find(inputTree map[string]any, path util.Path) (any, bool, error) {
   127  	scope.Debugf("Find path=%s", path)
   128  	if len(path) == 0 {
   129  		return nil, false, fmt.Errorf("path is empty")
   130  	}
   131  	node, found := find(inputTree, path)
   132  	return node, found, nil
   133  }
   134  
   135  // Delete sets value at path of input untyped tree to nil
   136  func Delete(root map[string]any, path util.Path) (bool, error) {
   137  	pc, _, err := getPathContext(&PathContext{Node: root}, path, path, false)
   138  	if err != nil {
   139  		return false, err
   140  	}
   141  	return true, WritePathContext(pc, nil, false)
   142  }
   143  
   144  // getPathContext is the internal implementation of GetPathContext.
   145  // If createMissing is true, it creates any missing map (but NOT list) path entries in root.
   146  func getPathContext(nc *PathContext, fullPath, remainPath util.Path, createMissing bool) (*PathContext, bool, error) {
   147  	scope.Debugf("getPathContext remainPath=%s, Node=%v", remainPath, nc.Node)
   148  	if len(remainPath) == 0 {
   149  		return nc, true, nil
   150  	}
   151  	pe := remainPath[0]
   152  
   153  	if nc.Node == nil {
   154  		if !createMissing {
   155  			return nil, false, fmt.Errorf("node %s is zero", pe)
   156  		}
   157  		if util.IsNPathElement(pe) || util.IsKVPathElement(pe) {
   158  			nc.Node = []any{}
   159  		} else {
   160  			nc.Node = make(map[string]any)
   161  		}
   162  	}
   163  
   164  	v := reflect.ValueOf(nc.Node)
   165  	if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
   166  		v = v.Elem()
   167  	}
   168  	ncNode := v.Interface()
   169  
   170  	// For list types, we need a key to identify the selected list item. This can be either a value key of the
   171  	// form :matching_value in the case of a leaf list, or a matching key:value in the case of a non-leaf list.
   172  	if lst, ok := ncNode.([]any); ok {
   173  		scope.Debug("list type")
   174  		// If the path element has the form [N], a list element is being selected by index. Return the element at index
   175  		// N if it exists.
   176  		if util.IsNPathElement(pe) {
   177  			idx, err := util.PathN(pe)
   178  			if err != nil {
   179  				return nil, false, fmt.Errorf("path %s, index %s: %s", fullPath, pe, err)
   180  			}
   181  			var foundNode any
   182  			if idx >= len(lst) || idx < 0 {
   183  				if !createMissing {
   184  					return nil, false, fmt.Errorf("index %d exceeds list length %d at path %s", idx, len(lst), remainPath)
   185  				}
   186  				idx = len(lst)
   187  				foundNode = make(map[string]any)
   188  			} else {
   189  				foundNode = lst[idx]
   190  			}
   191  			nn := &PathContext{
   192  				Parent: nc,
   193  				Node:   foundNode,
   194  			}
   195  			nc.KeyToChild = idx
   196  			return getPathContext(nn, fullPath, remainPath[1:], createMissing)
   197  		}
   198  
   199  		// Otherwise the path element must have form [key:value]. In this case, go through all list elements, which
   200  		// must have map type, and try to find one which has a matching key:value.
   201  		for idx, le := range lst {
   202  			// non-leaf list, expect to match item by key:value.
   203  			if lm, ok := le.(map[any]any); ok {
   204  				k, v, err := util.PathKV(pe)
   205  				if err != nil {
   206  					return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
   207  				}
   208  				if stringsEqual(lm[k], v) {
   209  					scope.Debugf("found matching kv %v:%v", k, v)
   210  					nn := &PathContext{
   211  						Parent: nc,
   212  						Node:   lm,
   213  					}
   214  					nc.KeyToChild = idx
   215  					nn.KeyToChild = k
   216  					if len(remainPath) == 1 {
   217  						scope.Debug("KV terminate")
   218  						return nn, true, nil
   219  					}
   220  					return getPathContext(nn, fullPath, remainPath[1:], createMissing)
   221  				}
   222  				continue
   223  			}
   224  			// repeat of the block above for the case where tree unmarshals to map[string]interface{}. There doesn't
   225  			// seem to be a way to merge this case into the above block.
   226  			if lm, ok := le.(map[string]any); ok {
   227  				k, v, err := util.PathKV(pe)
   228  				if err != nil {
   229  					return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
   230  				}
   231  				if stringsEqual(lm[k], v) {
   232  					scope.Debugf("found matching kv %v:%v", k, v)
   233  					nn := &PathContext{
   234  						Parent: nc,
   235  						Node:   lm,
   236  					}
   237  					nc.KeyToChild = idx
   238  					nn.KeyToChild = k
   239  					if len(remainPath) == 1 {
   240  						scope.Debug("KV terminate")
   241  						return nn, true, nil
   242  					}
   243  					return getPathContext(nn, fullPath, remainPath[1:], createMissing)
   244  				}
   245  				continue
   246  			}
   247  			// leaf list, expect path element [V], match based on value V.
   248  			v, err := util.PathV(pe)
   249  			if err != nil {
   250  				return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
   251  			}
   252  			if matchesRegex(v, le) {
   253  				scope.Debugf("found matching key %v, index %d", le, idx)
   254  				nn := &PathContext{
   255  					Parent: nc,
   256  					Node:   le,
   257  				}
   258  				nc.KeyToChild = idx
   259  				return getPathContext(nn, fullPath, remainPath[1:], createMissing)
   260  			}
   261  		}
   262  		return nil, false, fmt.Errorf("path %s: element %s not found", fullPath, pe)
   263  	}
   264  
   265  	if util.IsMap(ncNode) {
   266  		scope.Debug("map type")
   267  		var nn any
   268  		if m, ok := ncNode.(map[any]any); ok {
   269  			nn, ok = m[pe]
   270  			if !ok {
   271  				// remainPath == 1 means the patch is creation of a new leaf.
   272  				if createMissing || len(remainPath) == 1 {
   273  					m[pe] = make(map[any]any)
   274  					nn = m[pe]
   275  				} else {
   276  					return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
   277  				}
   278  			}
   279  		}
   280  		if reflect.ValueOf(ncNode).IsNil() {
   281  			ncNode = make(map[string]any)
   282  			nc.Node = ncNode
   283  		}
   284  		if m, ok := ncNode.(map[string]any); ok {
   285  			nn, ok = m[pe]
   286  			if !ok {
   287  				// remainPath == 1 means the patch is creation of a new leaf.
   288  				if createMissing || len(remainPath) == 1 {
   289  					nextElementNPath := len(remainPath) > 1 && util.IsNPathElement(remainPath[1])
   290  					if nextElementNPath {
   291  						scope.Debug("map type, slice child")
   292  						m[pe] = make([]any, 0)
   293  					} else {
   294  						scope.Debug("map type, map child")
   295  						m[pe] = make(map[string]any)
   296  					}
   297  					nn = m[pe]
   298  				} else {
   299  					return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
   300  				}
   301  			}
   302  		}
   303  
   304  		npc := &PathContext{
   305  			Parent: nc,
   306  			Node:   nn,
   307  		}
   308  		// for slices, use the address so that the slice can be mutated.
   309  		if util.IsSlice(nn) {
   310  			npc.Node = &nn
   311  		}
   312  		nc.KeyToChild = pe
   313  		return getPathContext(npc, fullPath, remainPath[1:], createMissing)
   314  	}
   315  
   316  	return nil, false, fmt.Errorf("leaf type %T in non-leaf Node %s", nc.Node, remainPath)
   317  }
   318  
   319  // setPathContext writes the given value to the Node in the given PathContext,
   320  // enlarging all PathContext lists to ensure all indexes are valid.
   321  func setPathContext(nc *PathContext, value any, merge bool) error {
   322  	processParent, err := setValueContext(nc, value, merge)
   323  	if err != nil || !processParent {
   324  		return err
   325  	}
   326  
   327  	// If the path included insertions, process them now
   328  	if nc.Parent.Parent == nil {
   329  		return nil
   330  	}
   331  	return setPathContext(nc.Parent, nc.Parent.Node, false) // note: tail recursive
   332  }
   333  
   334  // setValueContext writes the given value to the Node in the given PathContext.
   335  // If setting the value requires growing the final slice, grows it.
   336  func setValueContext(nc *PathContext, value any, merge bool) (bool, error) {
   337  	if nc.Parent == nil {
   338  		return false, nil
   339  	}
   340  
   341  	vv, mapFromString := tryToUnmarshalStringToYAML(value)
   342  
   343  	switch parentNode := nc.Parent.Node.(type) {
   344  	case *any:
   345  		switch vParentNode := (*parentNode).(type) {
   346  		case []any:
   347  			idx := nc.Parent.KeyToChild.(int)
   348  			if idx == -1 {
   349  				// Treat -1 as insert-at-end of list
   350  				idx = len(vParentNode)
   351  			}
   352  
   353  			if idx >= len(vParentNode) {
   354  				newElements := make([]any, idx-len(vParentNode)+1)
   355  				vParentNode = append(vParentNode, newElements...)
   356  				*parentNode = vParentNode
   357  			}
   358  
   359  			merged, err := mergeConditional(vv, nc.Node, merge)
   360  			if err != nil {
   361  				return false, err
   362  			}
   363  
   364  			vParentNode[idx] = merged
   365  			nc.Node = merged
   366  		default:
   367  			return false, fmt.Errorf("don't know about vtype %T", vParentNode)
   368  		}
   369  	case map[string]any:
   370  		key := nc.Parent.KeyToChild.(string)
   371  
   372  		// Update is treated differently depending on whether the value is a scalar or map type. If scalar,
   373  		// insert a new element into the terminal node, otherwise replace the terminal node with the new subtree.
   374  		if ncNode, ok := nc.Node.(*any); ok && !mapFromString {
   375  			switch vNcNode := (*ncNode).(type) {
   376  			case []any:
   377  				switch vv.(type) {
   378  				case map[string]any:
   379  					// the vv is a map, and the node is a slice
   380  					mergedValue := append(vNcNode, vv)
   381  					parentNode[key] = mergedValue
   382  				case *any:
   383  					merged, err := mergeConditional(vv, vNcNode, merge)
   384  					if err != nil {
   385  						return false, err
   386  					}
   387  
   388  					parentNode[key] = merged
   389  					nc.Node = merged
   390  				default:
   391  					// the vv is an basic JSON type (int, float, string, bool)
   392  					vv = append(vNcNode, vv)
   393  					parentNode[key] = vv
   394  					nc.Node = vv
   395  				}
   396  			default:
   397  				return false, fmt.Errorf("don't know about vnc type %T", vNcNode)
   398  			}
   399  		} else {
   400  			// For map passed as string type, the root is the new key.
   401  			if mapFromString {
   402  				if err := util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild); err != nil {
   403  					return false, err
   404  				}
   405  				vm := vv.(map[string]any)
   406  				newKey := getTreeRoot(vm)
   407  				return false, util.InsertIntoMap(nc.Parent.Node, newKey, vm[newKey])
   408  			}
   409  			parentNode[key] = vv
   410  			nc.Node = vv
   411  		}
   412  	// TODO `map[interface{}]interface{}` is used by tests in operator/cmd/mesh, we should add our own tests
   413  	case map[any]any:
   414  		key := nc.Parent.KeyToChild.(string)
   415  		parentNode[key] = vv
   416  		nc.Node = vv
   417  	default:
   418  		return false, fmt.Errorf("don't know about type %T", parentNode)
   419  	}
   420  
   421  	return true, nil
   422  }
   423  
   424  // mergeConditional returns a merge of newVal and originalVal if merge is true, otherwise it returns newVal.
   425  func mergeConditional(newVal, originalVal any, merge bool) (any, error) {
   426  	if !merge || util.IsValueNilOrDefault(originalVal) {
   427  		return newVal, nil
   428  	}
   429  	newS, err := yaml.Marshal(newVal)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  	if util.IsYAMLEmpty(string(newS)) {
   434  		return originalVal, nil
   435  	}
   436  	originalS, err := yaml.Marshal(originalVal)
   437  	if err != nil {
   438  		return nil, err
   439  	}
   440  	if util.IsYAMLEmpty(string(originalS)) {
   441  		return newVal, nil
   442  	}
   443  
   444  	mergedS, err := util.OverlayYAML(string(originalS), string(newS))
   445  	if err != nil {
   446  		return nil, err
   447  	}
   448  
   449  	if util.IsMap(originalVal) {
   450  		// For JSON compatibility
   451  		out := make(map[string]any)
   452  		if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
   453  			return nil, err
   454  		}
   455  		return out, nil
   456  	}
   457  	// For scalars and slices, copy the type
   458  	out := originalVal
   459  	if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
   460  		return nil, err
   461  	}
   462  	return out, nil
   463  }
   464  
   465  // find returns the value at path from the given tree, or false if the path does not exist.
   466  func find(treeNode any, path util.Path) (any, bool) {
   467  	if len(path) == 0 || treeNode == nil {
   468  		return nil, false
   469  	}
   470  	switch nt := treeNode.(type) {
   471  	case map[any]any:
   472  		val := nt[path[0]]
   473  		if val == nil {
   474  			return nil, false
   475  		}
   476  		if len(path) == 1 {
   477  			return val, true
   478  		}
   479  		return find(val, path[1:])
   480  	case map[string]any:
   481  		val := nt[path[0]]
   482  		if val == nil {
   483  			return nil, false
   484  		}
   485  		if len(path) == 1 {
   486  			return val, true
   487  		}
   488  		return find(val, path[1:])
   489  	case []any:
   490  		idx, err := strconv.Atoi(path[0])
   491  		if err != nil {
   492  			return nil, false
   493  		}
   494  		if idx >= len(nt) {
   495  			return nil, false
   496  		}
   497  		val := nt[idx]
   498  		return find(val, path[1:])
   499  	default:
   500  		return nil, false
   501  	}
   502  }
   503  
   504  // stringsEqual reports whether the string representations of a and b are equal. a and b may have different types.
   505  func stringsEqual(a, b any) bool {
   506  	return fmt.Sprint(a) == fmt.Sprint(b)
   507  }
   508  
   509  // matchesRegex reports whether str regex matches pattern.
   510  func matchesRegex(pattern, str any) bool {
   511  	match, err := regexp.MatchString(fmt.Sprint(pattern), fmt.Sprint(str))
   512  	if err != nil {
   513  		log.Errorf("bad regex expression %s", fmt.Sprint(pattern))
   514  		return false
   515  	}
   516  	scope.Debugf("%v regex %v? %v\n", pattern, str, match)
   517  	return match
   518  }
   519  
   520  // isSliceOrPtrInterface reports whether v is a slice, a ptr to slice or interface to slice.
   521  func isSliceOrPtrInterface(v any) bool {
   522  	vv := reflect.ValueOf(v)
   523  	if vv.Kind() == reflect.Ptr {
   524  		vv = vv.Elem()
   525  	}
   526  	if vv.Kind() == reflect.Interface {
   527  		vv = vv.Elem()
   528  	}
   529  	return vv.Kind() == reflect.Slice
   530  }
   531  
   532  // isMapOrInterface reports whether v is a map, or interface to a map.
   533  func isMapOrInterface(v any) bool {
   534  	vv := reflect.ValueOf(v)
   535  	if vv.Kind() == reflect.Interface {
   536  		vv = vv.Elem()
   537  	}
   538  	return vv.Kind() == reflect.Map
   539  }
   540  
   541  // tryToUnmarshalStringToYAML tries to unmarshal something that may be a YAML list or map into a structure. If not
   542  // possible, returns original scalar value.
   543  func tryToUnmarshalStringToYAML(s any) (any, bool) {
   544  	// If value type is a string it could either be a literal string or a map type passed as a string. Try to unmarshal
   545  	// to discover it's the latter.
   546  	vv := s
   547  
   548  	if reflect.TypeOf(vv).Kind() == reflect.String {
   549  		sv := strings.Split(vv.(string), "\n")
   550  		// Need to be careful not to transform string literals into maps unless they really are maps, since scalar handling
   551  		// is different for inserts.
   552  		if len(sv) == 1 && strings.Contains(s.(string), ": ") ||
   553  			len(sv) > 1 && strings.Contains(s.(string), ":") {
   554  			nv := make(map[string]any)
   555  			if err := json.Unmarshal([]byte(vv.(string)), &nv); err == nil {
   556  				// treat JSON as string
   557  				return vv, false
   558  			}
   559  			if err := yaml2.Unmarshal([]byte(vv.(string)), &nv); err == nil {
   560  				return nv, true
   561  			}
   562  		}
   563  	}
   564  	// looks like a literal or failed unmarshal, return original type.
   565  	return vv, false
   566  }
   567  
   568  // getTreeRoot returns the first key found in m. It assumes a single root tree.
   569  func getTreeRoot(m map[string]any) string {
   570  	for k := range m {
   571  		return k
   572  	}
   573  	return ""
   574  }