iot_server/internal/pkg/environment/variables.go

360 lines
11 KiB
Go

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