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 //}