github.com/verrazzano/verrazzano@v1.7.1/tools/vz/pkg/internal/util/json/json.go (about)

     1  // Copyright (c) 2021, 2024, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  // Package json handles ad-hoc JSON processing
     5  package json
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  
    18  	"go.uber.org/zap"
    19  )
    20  
    21  var jsonDataMap = make(map[string]interface{})
    22  var cacheMutex = &sync.Mutex{}
    23  var cacheHits = 0
    24  
    25  var matchAllArrayRe = regexp.MustCompile(`[[:alnum:]]*\[\]`)
    26  var matchIndexedArrayRe = regexp.MustCompile(`[[:alnum:]]*\[[[:digit:]]*\]`)
    27  
    28  type nodeInfo struct {
    29  	nodeName string // Name may be empty, denotes the current value is not keyed
    30  	isArray  bool
    31  	index    int // -1 indicates entire array, otherwise it is a specific non-negative integer index
    32  }
    33  
    34  // Note that for the K8S JSON data we are looking at we can use the k8s.io/api/core/v1 for that, so
    35  // this stuff is more for ad-hoc JSON that we encounter. For example, there could be cases where we have
    36  // snippets of JSON from a file or string that is otherwise unstructured, this can be used as a basic
    37  // mechanism to get at that information without needing to define formal structures to unmarshal it.
    38  // It also can pull in whole files in this manner, though for cases where there is a well-defined structure
    39  // we prefer using defined structures (ie: how we handle K8S JSON, etc...)
    40  
    41  // GetJSONDataFromBuffer Gets JsonData from a buffer. This is useful for when we extract a Json value out of something else. For
    42  // example if there is Json data in an otherwise unstructured log message that we are looking at. Helm version is
    43  // also an example where there is mixed text and Json, etc...
    44  func GetJSONDataFromBuffer(log *zap.SugaredLogger, buffer []byte) (jsonData interface{}, err error) {
    45  	err = json.Unmarshal(buffer, &jsonData)
    46  	if err != nil {
    47  		log.Debugf("Failed to unmarshal Json buffer", err)
    48  		return nil, err
    49  	}
    50  	log.Debugf("Successfully unmarshaled Json buffer")
    51  	return jsonData, nil
    52  }
    53  
    54  // GetJSONDataFromFile will open the specified JSON file, unmarshal it into a interface{}
    55  func GetJSONDataFromFile(log *zap.SugaredLogger, path string) (jsonData interface{}, err error) {
    56  
    57  	// Check the cache first
    58  	jsonData = getJSONDataIfPresent(path)
    59  	if jsonData != nil {
    60  		log.Debugf("Returning cached jsonData for %s", path)
    61  		return jsonData, nil
    62  	}
    63  
    64  	// Not found in the cache, get it from the file
    65  	file, err := os.Open(path)
    66  	if err != nil {
    67  		log.Debugf("Json file %s not found", path)
    68  		return nil, err
    69  	}
    70  	defer file.Close()
    71  
    72  	fileBytes, err := io.ReadAll(file)
    73  	if err != nil {
    74  		log.Debugf("Failed reading Json file %s", path)
    75  		return nil, err
    76  	}
    77  
    78  	jsonData, err = GetJSONDataFromBuffer(log, fileBytes)
    79  	if err != nil {
    80  		log.Debugf("Failed to unmarshal Json file %s", path, err)
    81  		return nil, err
    82  	}
    83  	log.Debugf("Successfully unmarshaled Json file %s", path)
    84  
    85  	// Cache it
    86  	putJSONDataIfNotPresent(path, jsonData)
    87  	return jsonData, err
    88  }
    89  
    90  // TBD: If we find we are relying heavily on this for some reason, we could look at existing packages we can use
    91  // along the lines of "jq" style querying
    92  // For now, adding simple support here to access value given a path.
    93  
    94  // GetJSONValue gets a JSON value
    95  func GetJSONValue(log *zap.SugaredLogger, jsonData interface{}, jsonPath string) (jsonValue interface{}, err error) {
    96  	// This is pretty dumb to start with here, it doesn't handle array lookups yet, etc...
    97  	// but I don't want to implement "jq" here either...
    98  	// TBD: Look at existing packages we can use
    99  	if jsonData == nil {
   100  		log.Debugf("No json data was supplied")
   101  		err = errors.New("No json data was supplied")
   102  		return nil, err
   103  	}
   104  
   105  	// If there is no path we return the current data back
   106  	if len(jsonPath) == 0 {
   107  		log.Debugf("No json path was supplied, return with json data supplied")
   108  		return jsonData, nil
   109  	}
   110  
   111  	// Separate the current node from the rest of the path here
   112  	pathTokens := strings.SplitN(jsonPath, ".", 2)
   113  	currentNodeInfo, err := getNodeInfo(log, pathTokens[0])
   114  	if err != nil {
   115  		log.Debugf("Failed getting the nodeInfo for %s", pathTokens[0], err)
   116  		return nil, err
   117  	}
   118  
   119  	// How we handle the data depends on the underlying type here, and whether we are at the end of the path
   120  	currentNode := jsonData
   121  	switch jsonData := jsonData.(type) {
   122  	case map[string]interface{}:
   123  		// Map interface we need to lookup the current node in the map
   124  		if len(currentNodeInfo.nodeName) == 0 {
   125  			// We have a map here without a key
   126  			log.Debugf("No key name for selecting from the map")
   127  			return nil, errors.New("No key name for selecting from the map")
   128  		}
   129  		currentNode = jsonData[currentNodeInfo.nodeName]
   130  		if currentNode == nil {
   131  			log.Debugf("Node not found %s", currentNodeInfo.nodeName)
   132  			err = fmt.Errorf("Node not found %s", currentNodeInfo.nodeName)
   133  			return nil, err
   134  		}
   135  	default:
   136  		// All other cases a key is not required and the currentNode is the jsonData
   137  	}
   138  
   139  	// If we are at the end of the path, return the current node
   140  	if len(pathTokens) == 1 {
   141  		switch currentNode := currentNode.(type) {
   142  		case []interface{}:
   143  			// Note that we can have a bare name supplied (without [] or [n] that ends up being an array when we find it, in those cases
   144  			// we treat it as the entire array
   145  			if !currentNodeInfo.isArray {
   146  				log.Debugf("node %s was not seen as having array syntax, type is array so treating it as entire array", pathTokens[0])
   147  				currentNodeInfo.isArray = true
   148  				currentNodeInfo.index = -1
   149  			}
   150  			log.Debugf("%s is an array", currentNodeInfo.nodeName)
   151  			nodesIn := currentNode
   152  			nodesInLen := len(nodesIn)
   153  			if currentNodeInfo.index < 0 {
   154  				nodesOut := make([]interface{}, nodesInLen)
   155  				copy(nodesOut, nodesIn)
   156  				return nodesOut, nil
   157  			}
   158  			// We get here if a specific array index was specified
   159  			if currentNodeInfo.index+1 > nodesInLen && nodesInLen > 0 {
   160  				log.Debugf("Index value out of range, found %d elements for %s[%d]", nodesInLen, currentNodeInfo.nodeName, currentNodeInfo.index)
   161  				return nil, fmt.Errorf("Index value out of range, found %d elements for %s[%d]", nodesInLen, currentNodeInfo.nodeName, currentNodeInfo.index)
   162  			}
   163  			return currentNode[currentNodeInfo.index], nil
   164  		default:
   165  			return currentNode, nil
   166  		}
   167  	}
   168  
   169  	// if we got here this is an intermediate "node", look it up in the current map and see how we need to process it
   170  	// it needs to be either an []interface{} or map[string]interface{}
   171  	// TODO: Not handling specific indexes in paths ie [], [N], etc... Currently you get the entire array if a node is
   172  	// an array
   173  	switch currentNode := currentNode.(type) {
   174  	case []interface{}:
   175  		// Note that we can have a bare name supplied (without [] or [n] that ends up being an array when we find it, in those cases
   176  		// we treat it as the entire array
   177  		if !currentNodeInfo.isArray {
   178  			currentNodeInfo.isArray = true
   179  			currentNodeInfo.index = -1
   180  		}
   181  		log.Debugf("%s is an array, indexing %d", currentNodeInfo.nodeName, currentNodeInfo.index)
   182  		nodesIn := currentNode
   183  		nodesInLen := len(nodesIn)
   184  		if currentNodeInfo.index < 0 {
   185  			nodesOut := make([]interface{}, nodesInLen)
   186  			for i, nodeIn := range nodesIn {
   187  				switch nodeIn := nodeIn.(type) {
   188  				case map[string]interface{}:
   189  					value, err := GetJSONValue(log, nodeIn, pathTokens[1])
   190  					if err != nil {
   191  						log.Debugf("%s failed to get array value at %d", currentNodeInfo.nodeName, i)
   192  						return nil, fmt.Errorf("%s failed to get array value at %d", currentNodeInfo.nodeName, i)
   193  					}
   194  					nodesOut[i] = value
   195  				default:
   196  					log.Debugf("%s array value type was not a non-terminal type %d", currentNodeInfo.nodeName, i)
   197  					return nil, fmt.Errorf("%s array value type was not a non-terminal type %d", currentNodeInfo.nodeName, i)
   198  				}
   199  			}
   200  			return nodesOut, nil
   201  		}
   202  		// We get here if a specific array index was specified
   203  		if currentNodeInfo.index+1 > nodesInLen && nodesInLen > 0 {
   204  			log.Debugf("Index value out of range, found %d elements for %s[%d]", nodesInLen, currentNodeInfo.nodeName, currentNodeInfo.index)
   205  			return nil, fmt.Errorf("Index value out of range, found %d elements for %s[%d]", nodesInLen, currentNodeInfo.nodeName, currentNodeInfo.index)
   206  		}
   207  		switch nodesIn[currentNodeInfo.index].(type) {
   208  		case map[string]interface{}:
   209  			value, err := GetJSONValue(log, nodesIn[currentNodeInfo.index].(map[string]interface{}), pathTokens[1])
   210  			if err != nil {
   211  				log.Debugf("%s failed to get array value at %d", currentNodeInfo.nodeName, currentNodeInfo.index)
   212  				return nil, fmt.Errorf("%s failed to get array value at %d", currentNodeInfo.nodeName, currentNodeInfo.index)
   213  			}
   214  			return value, nil
   215  		default:
   216  			log.Debugf("%s array value type was not a non-terminal type", currentNodeInfo.nodeName)
   217  			return nil, fmt.Errorf("%s array value type was not a non-terminal type", currentNodeInfo.nodeName)
   218  		}
   219  
   220  	case map[string]interface{}:
   221  		log.Debugf("%s type is a map, drilling down", currentNodeInfo.nodeName)
   222  		value, err := GetJSONValue(log, currentNode, pathTokens[1])
   223  		return value, err
   224  
   225  	default:
   226  		log.Debugf("%s type is not an intermediate type", currentNodeInfo.nodeName)
   227  		return nil, fmt.Errorf("%s type is not an intermediate type", currentNodeInfo.nodeName)
   228  	}
   229  }
   230  
   231  // TODO: Function to search Json return results (this may not be the best spot to put that, but noting that we need
   232  //       to have some helpers for doing that, seems likely to be a common operation to more precisely isolate
   233  //       a Json match). Will add that on the first case that needs it...
   234  
   235  // This extracts info from the node, mainly to determine if it is an array (entire or specific index into it)
   236  func getNodeInfo(log *zap.SugaredLogger, nodeString string) (info nodeInfo, err error) {
   237  	// Empty node string is allowed, it will only work with non-map types
   238  	if len(nodeString) == 0 {
   239  		return info, nil
   240  	}
   241  
   242  	// if it matches name[] or [] then we want the entire array, and trim the [] off
   243  	if matchAllArrayRe.MatchString(nodeString) {
   244  		info.isArray = true
   245  		info.index = -1
   246  		info.nodeName = strings.Split(nodeString, "[")[0]
   247  		log.Debugf("matched all array %s, returns: %v", nodeString, info)
   248  		return info, nil
   249  	}
   250  
   251  	// if it matches name[number] then we want the entire array, extract number, and trim the [number] off
   252  	if matchIndexedArrayRe.MatchString(nodeString) {
   253  		info.isArray = true
   254  		tokens := strings.Split(nodeString, "[")
   255  		info.nodeName = tokens[0]
   256  		indexStr := strings.Split(tokens[1], "]")[0]
   257  		i, err := strconv.Atoi(indexStr)
   258  		if err != nil {
   259  			log.Debugf("index string not an int %s", indexStr, err)
   260  			return info, err
   261  		}
   262  		if i < 0 {
   263  			log.Debugf("index string negative int %s", indexStr)
   264  			return info, errors.New("Negative array index is not allowed")
   265  		}
   266  		info.index = i
   267  		info.nodeName = strings.Split(nodeString, "[")[0]
   268  		log.Debugf("matched index into array %s, returns: %v", nodeString, info)
   269  		return info, nil
   270  	}
   271  	// For now not doing more error checking/handling here (can validate more), just return the name back (not an array)
   272  	info.nodeName = nodeString
   273  	return info, nil
   274  }
   275  
   276  // GetMatchingPathsWithValue TBD: This seems handy
   277  //func GetMatchingPathsWithValue(log *zap.SugaredLogger, jsonData map[string]interface{}, jsonPath string) (paths []string, err error) {
   278  //	return nil, nil
   279  //}
   280  
   281  func getJSONDataIfPresent(path string) (jsonData interface{}) {
   282  	cacheMutex.Lock()
   283  	jsonDataTest := jsonDataMap[path]
   284  	if jsonDataTest != nil {
   285  		jsonData = jsonDataTest
   286  		cacheHits++
   287  	}
   288  	cacheMutex.Unlock()
   289  	return jsonData
   290  }
   291  
   292  func putJSONDataIfNotPresent(path string, jsonData interface{}) {
   293  	cacheMutex.Lock()
   294  	jsonDataInMap := jsonDataMap[path]
   295  	if jsonDataInMap == nil {
   296  		jsonDataMap[path] = jsonData
   297  	}
   298  	cacheMutex.Unlock()
   299  }
   300  
   301  // TODO: Need to should make a more general json structure dump here for debugging
   302  //func debugMap(log *zap.SugaredLogger, mapIn map[string]interface{}) {
   303  //	log.Debugf("debugMap")
   304  //	for k, v := range mapIn {
   305  //		switch v.(type) {
   306  //		case string:
   307  //			log.Debugf("%i is string", k)
   308  //		case int:
   309  //			log.Debugf("%i is int", k)
   310  //		case float64:
   311  //			log.Debugf("%i is float64", k)
   312  //		case bool:
   313  //			log.Debugf("%i is bool", k)
   314  //		case []interface{}:
   315  //			log.Debugf("%i is an []interface{}:", k)
   316  //		case map[string]interface{}:
   317  //			log.Debugf("%i is map[string]interface{}", k)
   318  //		case nil:
   319  //			log.Debugf("%i is nil", k)
   320  //		default:
   321  //			log.Debugf("%i is unknown type")
   322  //		}
   323  //	}
   324  //}