config/config_map.go (195 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package config import ( "encoding/json" "fmt" "log" "os" "reflect" "strconv" "strings" ) // A ConfigMap is a map where the keys are in the form of: A_KEY_WITH_UNDERSCORES. // The map splits the key by the underscores and creates a nested map that // represents the key. For example, the key "A_KEY_WITH_UNDERSCORES" would be // represented as: // // { // "a": { // "key": { // "with": { // "underscores": "value", // }, // }, // }, // } // // To interact with the ConfigMap, use the Insert, Get, and Delete by passing // keys in the form above. Only the config map is modified by these functions. // The opsRootConfig map is only used to read the config keys in opsroot.json. // The pluginOpsRootConfigs map is only used to read the config keys in // plugins (from their opsroot.json). It is a map that maps the plugin name to // the config map for that plugin. type ConfigMap struct { pluginOpsRootConfigs map[string]map[string]interface{} opsRootConfig map[string]interface{} config map[string]interface{} configPath string } // Insert inserts a key and value into the ConfigMap. If the key already exists, // the value is overwritten. The expected key format is A_KEY_WITH_UNDERSCORES. func (c *ConfigMap) Insert(key string, value string) error { keys, err := parseKey(strings.ToLower(key)) if err != nil { return err } currentMap := c.config lastIndex := len(keys) - 1 for i, subKey := range keys { // If we are at the last key, set the value if i == lastIndex { v, err := parseValue(value) if err != nil { return err } currentMap[subKey] = v } else { // If the sub-map doesn't exist, create it if _, ok := currentMap[subKey]; !ok { currentMap[subKey] = make(map[string]interface{}) } // Update the current map to the sub-map m, ok := currentMap[subKey].(map[string]interface{}) if !ok { return fmt.Errorf("invalid key: '%s' - '%s' is already being used for a value", key, subKey) } currentMap = m } } return nil } func (c *ConfigMap) Get(key string) (string, error) { cmap := c.Flatten() val, ok := cmap[key] if !ok { return "", fmt.Errorf("invalid key: '%s' - key does not exist", key) } return val, nil } func (c *ConfigMap) Delete(key string) error { delFunc := func(config map[string]interface{}, key string) bool { if _, ok := config[key]; !ok { return false } delete(config, key) return true } keys, err := parseKey(strings.ToLower(key)) if err != nil { return err } ok := visit(c.config, 0, keys, delFunc) if !ok { return fmt.Errorf("invalid key: '%s' - key does not exist in config.json", key) } return nil } func (c *ConfigMap) Flatten() map[string]string { outputMap := make(map[string]string) merged := mergeMaps(c.opsRootConfig, c.config) for name, pluginConfig := range c.pluginOpsRootConfigs { // edge case: check that merged does not contain name already if _, ok := merged[name]; ok { log.Printf("config has key with same name as plugin %s. Plugin config will be ignored.", name) continue } merged[name] = pluginConfig } flatten("", merged, outputMap) return outputMap } func (c *ConfigMap) SaveConfig() error { var configJSON, err = json.MarshalIndent(c.config, "", " ") if err != nil { return err } return os.WriteFile(c.configPath, configJSON, 0644) } // /// func flatten(prefix string, inputMap map[string]interface{}, outputMap map[string]string) { if len(prefix) > 0 { prefix += "_" } for k, v := range inputMap { key := strings.ToUpper(prefix + k) switch child := v.(type) { case map[string]interface{}: flatten(key, child, outputMap) default: outputMap[key] = formatValue(v) } } } func formatValue(v interface{}) string { switch val := v.(type) { case int, int8, int16, int32, int64: return fmt.Sprintf("%d", val) case uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", val) case float32, float64: return strconv.FormatFloat(reflect.ValueOf(val).Float(), 'f', -1, 64) case bool: return strconv.FormatBool(val) case string: return val default: return fmt.Sprintf("%v", val) } } type configOperationFunc func(config map[string]interface{}, key string) bool func visit(config map[string]interface{}, index int, keys []string, f configOperationFunc) bool { // base case: if the key is the last key in the list, call the function f if index == len(keys)-1 { return f(config, keys[index]) } // recursive case: if the key is not the last key in the list, call visit on the next key (if cast ok) conf, ok := config[keys[index]].(map[string]interface{}) if !ok { return false } success := visit(conf, index+1, keys, f) // if the parent map is empty, clean up if success && len(conf) == 0 { delete(config, keys[index]) } return success } func parseKey(key string) ([]string, error) { parts := strings.Split(key, "_") for _, part := range parts { if part == "" { return nil, fmt.Errorf("invalid key: %s", key) } } return parts, nil } /* VALUEs are parsed in the following way: - try to parse as a jsos first, and if it is a json, store as a json - then try to parse as a number, and if it is a (float) number store as a number - then try to parse as true or false and store as a boolean - then check if it's null and store as a null - otherwise store as a string */ func parseValue(value string) (interface{}, error) { // Try to parse as json var jsonValue interface{} if err := json.Unmarshal([]byte(value), &jsonValue); err == nil { return jsonValue, nil } // Try to parse as a integer with strconv if intValue, err := strconv.Atoi(value); err == nil { return intValue, nil } // Try to parse as a float with strconv if floatValue, err := strconv.ParseFloat(value, 64); err == nil { return floatValue, nil } // Try to parse as a boolean if value == "true" || value == "false" { return value == "true", nil } // Try to parse as null if value == "null" { return nil, nil } // Otherwise, return the string return value, nil } // mergeMaps merges map2 into map1 overwriting any values in map1 with values from map2 // when there are conflicts. It returns the merged map. func mergeMaps(map1, map2 map[string]interface{}) map[string]interface{} { if len(map1) == 0 { return map2 } if len(map2) == 0 { return map1 } mergedMap := make(map[string]interface{}) for key, value := range map1 { map2Value, ok := map2[key] // key doesn't exist in map2 so add it to the merged map if !ok { mergedMap[key] = value continue } // key exists in map2 but map1 value is NOT a map, so add value from map2 mapFromMap1, ok := value.(map[string]interface{}) if !ok { mergedMap[key] = map2Value continue } mapFromMap2, ok := map2Value.(map[string]interface{}) // key exists in map2, map1 value IS a map but map2 value is not, so overwrite with map2 if !ok { mergedMap[key] = mapFromMap2 continue } // key exists in map2, map1 value IS a map, map2 value IS a map, so merge recursively mergedMap[key] = mergeMaps(mapFromMap1, mapFromMap2) } // add any keys that exist in map2 but not in map1 for key, value := range map2 { if _, ok := mergedMap[key]; !ok { mergedMap[key] = value } } return mergedMap }