/******************************************************************************* * Copyright 2019 Dell Inc. * Copyright 2020 Intel Inc. * * Licensed 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 environment import ( "fmt" //"github.com/pelletier/go-toml/v2" "github.com/winc-link/hummingbird/internal/pkg/config" "github.com/winc-link/hummingbird/internal/pkg/logger" "os" "reflect" "strconv" "strings" "github.com/pelletier/go-toml" ) const ( bootTimeoutSecondsDefault = 60 bootRetrySecondsDefault = 1 defaultConfDirValue = "./res" envKeyConfigUrl = "EDGEX_CONFIGURATION_PROVIDER" envKeyUseRegistry = "EDGEX_USE_REGISTRY" envKeyStartupDuration = "EDGEX_STARTUP_DURATION" envKeyStartupInterval = "EDGEX_STARTUP_INTERVAL" envConfDir = "EDGEX_CONF_DIR" envProfile = "EDGEX_PROFILE" envFile = "EDGEX_CONFIG_FILE" ) // Variables is receiver that holds Variables variables and encapsulates toml.Tree-based configuration field // overrides. Assumes "_" embedded in Variables variable key separates sub-structs; e.g. foo_bar_baz might refer to // // type foo struct { // bar struct { // baz string // } // } type Variables struct { variables map[string]string lc logger.LoggingClient } // NewEnvironment constructor reads/stores os.Environ() for use by Variables receiver methods. func NewVariables() *Variables { osEnv := os.Environ() e := &Variables{ variables: make(map[string]string, len(osEnv)), } for _, env := range osEnv { // Can not use Split() on '=' since the value may have an '=' in it, so changed to use Index() index := strings.Index(env, "=") if index == -1 { continue } key := env[:index] value := env[index+1:] e.variables[key] = value } return e } // UseRegistry returns whether the envKeyUseRegistry key is set to true and whether the override was used func (e *Variables) UseRegistry() (bool, bool) { value := os.Getenv(envKeyUseRegistry) if len(value) == 0 { return false, false } logEnvironmentOverride(e.lc, "-r/--registry", envKeyUseRegistry, value) return value == "true", true } // OverrideConfiguration method replaces values in the configuration for matching Variables variable keys. // serviceConfig must be pointer to the service configuration. func (e *Variables) OverrideConfiguration(serviceConfig interface{}) (int, error) { var overrideCount = 0 contents, err := toml.Marshal(reflect.ValueOf(serviceConfig).Elem().Interface()) if err != nil { return 0, err } configTree, err := toml.LoadBytes(contents) if err != nil { return 0, err } // The toml.Tree API keys() only return to top level keys, rather that paths. // It is also missing a GetPaths so have to spin our own paths := e.buildPaths(configTree.ToMap()) // Now that we have all the paths in the config tree, we need to create a map that has the uppercase versions as // the map keys and the original versions as the map values so we can match against uppercase names but use the // originals to set values. pathMap := e.buildUppercasePathMap(paths) for envVar, envValue := range e.variables { envKey := strings.Replace(envVar, "_", ".", -1) key, found := e.getKeyForMatchedPath(pathMap, envKey) if !found { continue } oldValue := configTree.Get(key) newValue, err := e.convertToType(oldValue, envValue) if err != nil { return 0, fmt.Errorf("environment value override failed for %s=%s: %s", envVar, envValue, err.Error()) } configTree.Set(key, newValue) overrideCount++ logEnvironmentOverride(e.lc, key, envVar, envValue) } // Put the configuration back into the services configuration struct with the overridden values err = configTree.Unmarshal(serviceConfig) if err != nil { return 0, fmt.Errorf("could not marshal toml configTree to configuration: %s", err.Error()) } return overrideCount, nil } // buildPaths create the path strings for all settings in the Config tree's key map func (e *Variables) buildPaths(keyMap map[string]interface{}) []string { var paths []string for key, item := range keyMap { if reflect.TypeOf(item).Kind() != reflect.Map { paths = append(paths, key) continue } subMap := item.(map[string]interface{}) subPaths := e.buildPaths(subMap) for _, path := range subPaths { paths = append(paths, fmt.Sprintf("%s.%s", key, path)) } } return paths } // buildUppercasePathMap builds a map where the key is the uppercase version of the path // and the value is original version of the path func (e *Variables) buildUppercasePathMap(paths []string) map[string]string { ucMap := make(map[string]string) for _, path := range paths { ucMap[strings.ToUpper(path)] = path } return ucMap } // getKeyForMatchedPath searches for match of the environment variable name with the uppercase path (pathMap keys) // If matched found to original path (pathMap values) is returned as the "key" // For backward compatibility a case insensitive comparison is currently used. func (e *Variables) getKeyForMatchedPath(pathMap map[string]string, envVarName string) (string, bool) { for ucKey, lcKey := range pathMap { if ucKey == envVarName { return lcKey, true } } return "", false } // OverrideConfigProviderInfo overrides the Configuration Provider ServiceConfig values // from an Variables variable value (if it exists). func (e *Variables) OverrideConfigProviderInfo(configProviderInfo config.ServiceConfig) (config.ServiceConfig, error) { url := os.Getenv(envKeyConfigUrl) if len(url) > 0 { logEnvironmentOverride(e.lc, "Configuration Provider Information", envKeyConfigUrl, url) if err := configProviderInfo.PopulateFromUrl(url); err != nil { return config.ServiceConfig{}, err } } return configProviderInfo, nil } // convertToType attempts to convert the string value to the specified type of the old value func (_ *Variables) convertToType(oldValue interface{}, value string) (newValue interface{}, err error) { switch oldValue.(type) { case []string: newValue = parseCommaSeparatedSlice(value) case []interface{}: newValue = parseCommaSeparatedSlice(value) case string: newValue = value case bool: newValue, err = strconv.ParseBool(value) case int: newValue, err = strconv.ParseInt(value, 10, strconv.IntSize) newValue = int(newValue.(int64)) case int8: newValue, err = strconv.ParseInt(value, 10, 8) newValue = int8(newValue.(int64)) case int16: newValue, err = strconv.ParseInt(value, 10, 16) newValue = int16(newValue.(int64)) case int32: newValue, err = strconv.ParseInt(value, 10, 32) newValue = int32(newValue.(int64)) case int64: newValue, err = strconv.ParseInt(value, 10, 64) case uint: newValue, err = strconv.ParseUint(value, 10, strconv.IntSize) newValue = uint(newValue.(uint64)) case uint8: newValue, err = strconv.ParseUint(value, 10, 8) newValue = uint8(newValue.(uint64)) case uint16: newValue, err = strconv.ParseUint(value, 10, 16) newValue = uint16(newValue.(uint64)) case uint32: newValue, err = strconv.ParseUint(value, 10, 32) newValue = uint32(newValue.(uint64)) case uint64: newValue, err = strconv.ParseUint(value, 10, 64) case float32: newValue, err = strconv.ParseFloat(value, 32) newValue = float32(newValue.(float64)) case float64: newValue, err = strconv.ParseFloat(value, 64) default: err = fmt.Errorf( "configuration type of '%s' is not supported for environment variable override", reflect.TypeOf(oldValue).String()) } return newValue, err } // StartupInfo provides the startup timer values which are applied to the StartupTimer created at boot. type StartupInfo struct { Duration int Interval int } // GetStartupInfo gets the Service StartupInfo values from an Variables variable value (if it exists) // or uses the default values. func GetStartupInfo(serviceKey string) StartupInfo { // Logger hasn't be created at the time this info is needed so have to create local client. lc := logger.NewClient(serviceKey, logger.DefaultLogLevel, logger.DefaultLogPath) startup := StartupInfo{ Duration: bootTimeoutSecondsDefault, Interval: bootRetrySecondsDefault, } // Get the startup timer configuration from environment, if provided. value := os.Getenv(envKeyStartupDuration) if len(value) > 0 { logEnvironmentOverride(lc, "Startup Duration", envKeyStartupDuration, value) if n, err := strconv.ParseInt(value, 10, 0); err == nil && n > 0 { startup.Duration = int(n) } } // Get the startup timer interval, if provided. value = os.Getenv(envKeyStartupInterval) if len(value) > 0 { logEnvironmentOverride(lc, "Startup Interval", envKeyStartupInterval, value) if n, err := strconv.ParseInt(value, 10, 0); err == nil && n > 0 { startup.Interval = int(n) } } return startup } // GetConfDir get the config directory value from an Variables variable value (if it exists) // or uses passed in value or default if previous result in blank. func GetConfDir(lc logger.LoggingClient, configDir string) string { envValue := os.Getenv(envConfDir) if len(envValue) > 0 { configDir = envValue logEnvironmentOverride(lc, "-c/-confdir", envFile, envValue) } if len(configDir) == 0 { configDir = defaultConfDirValue } return configDir } // GetProfileDir get the profile directory value from an Variables variable value (if it exists) // or uses passed in value or default if previous result in blank. func GetProfileDir(lc logger.LoggingClient, profileDir string) string { envValue := os.Getenv(envProfile) if len(envValue) > 0 { profileDir = envValue logEnvironmentOverride(lc, "-p/-profile", envProfile, envValue) } if len(profileDir) > 0 { profileDir += "/" } return profileDir } // GetConfigFileName gets the configuration filename value from an Variables variable value (if it exists) // or uses passed in value. func GetConfigFileName(lc logger.LoggingClient, configFileName string) string { envValue := os.Getenv(envFile) if len(envValue) > 0 { configFileName = envValue logEnvironmentOverride(lc, "-f/-file", envFile, envValue) } return configFileName } // parseCommaSeparatedSlice converts comma separated list to a string slice func parseCommaSeparatedSlice(value string) (values []interface{}) { // Assumption is environment variable value is comma separated // Whitespace can vary so must be trimmed out result := strings.Split(strings.TrimSpace(value), ",") for _, entry := range result { values = append(values, strings.TrimSpace(entry)) } return values } // logEnvironmentOverride logs that an option or configuration has been override by an environment variable. func logEnvironmentOverride(lc logger.LoggingClient, name string, key string, value string) { if lc == nil { fmt.Println(fmt.Sprintf("Variables override of '%s' by environment variable: %s=%s", name, key, value)) return } lc.Info(fmt.Sprintf("Variables override of '%s' by environment variable: %s=%s", name, key, value)) }