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