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
}