360 lines
11 KiB
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))
|
||
|
}
|