From a041e08e06629d1b14a7adb8f0ee40071d9948bb Mon Sep 17 00:00:00 2001 From: lenny Date: Mon, 10 Feb 2020 16:07:02 -0700 Subject: [PATCH] feat: Add Self seeding, env var overrides, cmd-line options per ADR 0005-Service-Self-Config.md closes #14 Signed-off-by: lenny --- bootstrap/bootstrap.go | 107 +++++++++--- bootstrap/config/config.go | 47 +++++- bootstrap/config/environment.go | 213 +++++++++++++++++++++--- bootstrap/config/environment_test.go | 179 +++++++++++++++++--- bootstrap/config/file.go | 30 +++- bootstrap/config/provider.go | 7 +- bootstrap/config/provider_test.go | 20 ++- bootstrap/flags/flags.go | 63 ++++++- bootstrap/flags/flags_test.go | 18 +- bootstrap/registration/registry.go | 78 +++++++-- bootstrap/registration/registry_test.go | 169 +++++++++++++++++++ go.mod | 5 +- 12 files changed, 815 insertions(+), 121 deletions(-) create mode 100644 bootstrap/registration/registry_test.go diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 486b2094..b5d74db7 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -19,9 +19,14 @@ import ( "fmt" "os" "os/signal" + "reflect" "sync" "syscall" + "github.com/edgexfoundry/go-mod-configuration/configuration" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/edgexfoundry/go-mod-registry/registry" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/config" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/flags" @@ -30,12 +35,6 @@ import ( "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/registration" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/di" - - "github.com/edgexfoundry/go-mod-configuration/configuration" - - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - - "github.com/edgexfoundry/go-mod-registry/registry" ) // Deferred defines the signature of a function returned by RunAndReturnWaitGroup that should be executed via defer. @@ -93,26 +92,78 @@ func RunAndReturnWaitGroup( var wg sync.WaitGroup deferred := func() {} + lc := logging.FactoryToStdout(serviceKey) + + // Enforce serviceConfig (which is an interface) is a pointer so we can dereference it later with confidence when required. + if reflect.TypeOf(serviceConfig).Kind() != reflect.Ptr { + fatalError(fmt.Errorf("serviceConfig parameter must be a pointer to the service's configuration struct"), lc) + } + translateInterruptToCancel(ctx, &wg, cancel) configFileName := commonFlags.ConfigFileName() - lc := logging.FactoryToStdout(serviceKey) - // Create new ProviderInfo and initialize it from command-line flag or Environment variable - configProviderInfo, err := config.NewProviderInfo(lc, commonFlags.ConfigProviderUrl()) - if err != nil { - fatalError(err, lc) + // TODO: remove this check once -r/-registry is back to a bool in release v2.0.0 + if len(commonFlags.ConfigProviderUrl()) > 0 && len(commonFlags.RegistryUrl()) > 0 { + fatalError(fmt.Errorf("use of -cp/-configProvider with -r/-registry= not premitted"), lc) } // override file-based configuration with environment variables. bootstrapConfig := serviceConfig.GetBootstrap() - startupInfo := config.OverrideStartupInfoFromEnvironment(lc, bootstrapConfig.Startup) + environment := config.NewEnvironment() + startupInfo := environment.OverrideStartupInfo(lc, bootstrapConfig.Startup) // Update the startup timer to reflect whatever configuration read, if anything available. startupTimer.UpdateTimer(startupInfo.Duration, startupInfo.Interval) + // Local configuration must be loaded first in case need registry config info and/or + // need to push it to the Configuration Provider. + err = config.LoadFromFile( + lc, + commonFlags.ConfigDirectory(), + commonFlags.Profile(), + configFileName, + serviceConfig, + ) + if err != nil { + fatalError(err, lc) + } + + // Environment variable overrides have precedence over all others, + // so make sure they are applied before config is used for anything. + overrideCount, err := environment.OverrideConfiguration(lc, serviceConfig) + if err != nil { + fatalError(err, lc) + } + + configProviderUrl := commonFlags.ConfigProviderUrl() + // TODO: remove this check once -r/-registry is back to a bool and only enable registry usage in release v2.0.0 + // For backwards compatibility with Fuji device and app services that use just -r/-registry for both registry and config + if len(configProviderUrl) == 0 && commonFlags.UseRegistry() { + if len(commonFlags.RegistryUrl()) > 0 { + configProviderUrl = commonFlags.RegistryUrl() + lc.Info("Config Provider URL created from -r/-registry= flag") + } else { + // Have to use the Registry config for Configuration provider + registryConfig := serviceConfig.GetBootstrap().Registry + configProviderUrl = fmt.Sprintf("%s.http://%s:%d", registryConfig.Type, registryConfig.Host, registryConfig.Port) + lc.Info("Config Provider URL created from Registry configuration") + } + } + + // Create new ProviderInfo and initialize it from command-line flag or Environment variables + configProviderInfo, err := config.NewProviderInfo(lc, environment, configProviderUrl) + if err != nil { + fatalError(err, lc) + } + switch configProviderInfo.UseProvider() { case true: + lc.Info(fmt.Sprintf( + "Using Configuration provider (%s) from: %s", + configProviderInfo.ServiceConfig().Type, + configProviderInfo.ServiceConfig().GetUrl())) + var configClient configuration.Client // set up configClient; use it to load configuration from provider. @@ -122,36 +173,38 @@ func RunAndReturnWaitGroup( configProviderInfo.ServiceConfig(), serviceConfig, configStem, + commonFlags.OverwriteConfig(), lc, serviceKey, + environment, + overrideCount, ) if err != nil { fatalError(err, lc) } + lc = logging.FactoryFromConfiguration(serviceKey, serviceConfig) config.ListenForChanges(ctx, &wg, serviceConfig, lc, configClient, configUpdatedStream) - lc.Info(fmt.Sprintf("Loaded configuration from %s", configProviderInfo.ServiceConfig().GetUrl())) case false: - // load configuration from file. - err = config.LoadFromFile( - lc, - commonFlags.ConfigDirectory(), - commonFlags.Profile(), - configFileName, - serviceConfig, - ) - if err != nil { - fatalError(err, lc) - } lc = logging.FactoryFromConfiguration(serviceKey, serviceConfig) + config.LogConfigInfo(lc, "Using local configuration from file", overrideCount, serviceKey) } var registryClient registry.Client - // setup registryClient if it is enabled - if commonFlags.UseRegistry() { - registryClient, err = registration.RegisterWithRegistry(ctx, startupTimer, serviceConfig, lc, serviceKey) + // TODO: Remove `|| config.UseRegistry()` for release V2.0.0 + if commonFlags.UseRegistry() || environment.UseRegistry() { + // For backwards compatibility with Fuji Device Service, registry is a string that can contain a provider URL. + // TODO: Remove registryUrl in call below for release V2.0.0 + registryClient, err = registration.RegisterWithRegistry( + ctx, + startupTimer, + serviceConfig, + commonFlags.RegistryUrl(), + environment, + lc, + serviceKey) if err != nil { fatalError(err, lc) } diff --git a/bootstrap/config/config.go b/bootstrap/config/config.go index d7fe0e99..a681e238 100644 --- a/bootstrap/config/config.go +++ b/bootstrap/config/config.go @@ -18,6 +18,7 @@ import ( "context" "errors" "fmt" + "reflect" "sync" "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" @@ -25,7 +26,6 @@ import ( "github.com/edgexfoundry/go-mod-configuration/configuration" configTypes "github.com/edgexfoundry/go-mod-configuration/pkg/types" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" ) @@ -44,7 +44,7 @@ func createClient( return configuration.NewConfigurationClient(providerConfig) } -// UpdateFromProvider connects to the config provider, gets configuration, and updates the service's +// UpdateFromProvider connects to the configuration provider, puts or gets configuration, and updates the service's // configuration struct. func UpdateFromProvider( ctx context.Context, @@ -52,21 +52,47 @@ func UpdateFromProvider( providerConfig configTypes.ServiceConfig, serviceConfig interfaces.Configuration, configStem string, + overwriteConfig bool, lc logger.LoggingClient, - serviceKey string) (configuration.Client, error) { + serviceKey string, + environment *Environment, + overrideCount int) (configuration.Client, error) { var updateFromConfigProvider = func(configClient configuration.Client) error { if !configClient.IsAlive() { return errors.New("configuration provider is not available") } - rawConfig, err := configClient.GetConfiguration(serviceConfig) + hasConfig, err := configClient.HasConfiguration() if err != nil { - return fmt.Errorf("could not get configuration from Registry: %v", err.Error()) + return fmt.Errorf("could not determine if Configuration provider has configuration: %s", err.Error()) } - if !serviceConfig.UpdateFromRaw(rawConfig) { - return errors.New("configuration from Registry failed type check") + if !hasConfig || overwriteConfig { + // Environment overrides already applied previously so just push to Configuration Provider + // Note that serviceConfig is a pointer, so we have to use reflection to dereference it. + err = configClient.PutConfiguration(reflect.ValueOf(serviceConfig).Elem().Interface(), true) + if err != nil { + return fmt.Errorf("could not push configuration into Configuration Provider: %s", err.Error()) + } + + LogConfigInfo(lc, "Configuration has been pushed to into Configuration Provider", overrideCount, serviceKey) + } else { + rawConfig, err := configClient.GetConfiguration(serviceConfig) + if err != nil { + return fmt.Errorf("could not get configuration from Configuration provider: %s", err.Error()) + } + + if !serviceConfig.UpdateFromRaw(rawConfig) { + return errors.New("configuration from Configuration provider failed type check") + } + + overrideCount, err := environment.OverrideConfiguration(lc, serviceConfig) + if err != nil { + return err + } + + LogConfigInfo(lc, "Configuration has been pulled from Configuration provider", overrideCount, serviceKey) } return nil @@ -93,7 +119,12 @@ func UpdateFromProvider( return nil, errors.New("unable to update configuration from provider in allotted time") } -// ListenForChanges leverages the registry client's WatchForChanges() method to receive changes to and update the +// LogConfigInfo logs the config info message with number over overrides that occurred and the service key that was used. +func LogConfigInfo(lc logger.LoggingClient, message string, overrideCount int, serviceKey string) { + lc.Info(fmt.Sprintf("%s (%d environment overrides applied)", message, overrideCount), "service key", serviceKey) +} + +// ListenForChanges leverages the Configuration Provider client's WatchForChanges() method to receive changes to and update the // service's configuration struct's writable sub-struct. It's assumed the log level is universally part of the // writable struct and this function explicitly updates the loggingClient's log level when new configuration changes // are received. diff --git a/bootstrap/config/environment.go b/bootstrap/config/environment.go index 10fbda74..e15ed483 100644 --- a/bootstrap/config/environment.go +++ b/bootstrap/config/environment.go @@ -17,61 +17,228 @@ package config import ( "fmt" "os" + "reflect" "strconv" - - "github.com/edgexfoundry/go-mod-bootstrap/config" + "strings" "github.com/edgexfoundry/go-mod-configuration/pkg/types" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/pelletier/go-toml" + + "github.com/edgexfoundry/go-mod-bootstrap/config" ) const ( - envKeyUrl = "edgex_configuration_provider" - envKeyStartupDuration = "startup_duration" - envKeyStartupInterval = "startup_interval" + envKeyConfigUrl = "EDGEX_CONFIGURATION_PROVIDER" + envKeyRegistryUrl = "edgex_registry" // TODO: Remove for v2.0.0 + envKeyStartupDuration = "startup_duration" // TODO: Change to EDGEX_STARTUP_DURATION for v2.0.0 + envKeyStartupInterval = "startup_interval" // TODO: Change to EDGEX_STARTUP_INTERVAL for v2.0.0 ) -// OverrideConfigProviderInfoFromEnvironment overrides the Configuration Provider ServiceConfig values -// from an environment variable value (if it exists). -func OverrideConfigProviderInfoFromEnvironment( +// Environment is receiver that holds Environment variables and encapsulates toml.Tree-based configuration field +// overrides. Assumes "_" embedded in Environment variable key separates substructs; e.g. foo_bar_baz might refer to +// +// type foo struct { +// bar struct { +// baz string +// } +// } +type Environment struct { + env map[string]string +} + +// NewEnvironment constructor reads/stores os.Environ() for use by Environment receiver methods. +func NewEnvironment() *Environment { + osEnv := os.Environ() + e := &Environment{ + env: make(map[string]string, len(osEnv)), + } + for _, env := range osEnv { + kv := strings.Split(env, "=") + if len(kv) == 2 && len(kv[0]) > 0 && len(kv[1]) > 0 { + e.env[kv[0]] = kv[1] + } + } + return e +} + +// UseRegistry returns whether the envKeyRegistryUrl key is set +// TODO: remove this func for release v2.0.0 when envKeyRegistryUrl is removed +func (e *Environment) UseRegistry() bool { + _, ok := os.LookupEnv(envKeyRegistryUrl) + return ok +} + +// OverrideConfiguration method replaces values in the configuration for matching Environment variable keys. +// serviceConfig must be pointer to the service configuration. +func (e *Environment) OverrideConfiguration(lc logger.LoggingClient, 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 + } + + for envVar, envValue := range e.env { + key := strings.Replace(envVar, "_", ".", -1) + switch { + case configTree.Has(key): + 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++ + lc.Info(fmt.Sprintf("Environment varable override of %s by: %s=%s", 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 +} + +// OverrideConfigProviderInfo overrides the Configuration Provider ServiceConfig values +// from an Environment variable value (if it exists). +func (_ *Environment) OverrideConfigProviderInfo( lc logger.LoggingClient, configProviderInfo types.ServiceConfig) (types.ServiceConfig, error) { - // Override the configuration provider info, if provided. - if env := os.Getenv(envKeyUrl); env != "" { - lc.Info(fmt.Sprintf("Overriding Confiuragtion Provider information from environment variable. %s=%s", envKeyUrl, env)) + if url := os.Getenv(envKeyConfigUrl); len(url) > 0 { + lc.Info(fmt.Sprintf("Confiuragtion Provider information overridden by Environment variable: %s=%s", envKeyConfigUrl, url)) - if err := configProviderInfo.PopulateFromUrl(env); err != nil { + if err := configProviderInfo.PopulateFromUrl(url); err != nil { return types.ServiceConfig{}, err } + } else { + // TODO: Remove this for release V2.0.0 + // This is for backwards compatibility with Fuji Device Services. + // If --registry= is used then we must use the for the configuration provider. + if url := os.Getenv(envKeyRegistryUrl); len(url) > 0 { + lc.Info(fmt.Sprintf("Confiuragtion Provider information overridden by Environment variable: %s=%s", envKeyRegistryUrl, url)) + + if err := configProviderInfo.PopulateFromUrl(url); err != nil { + return types.ServiceConfig{}, err + } + } } return configProviderInfo, nil } -// OverrideStartupInfoFromEnvironment overrides the Service StartupInfo values from an environment variable value (if it exists). -func OverrideStartupInfoFromEnvironment( +// TODO: Remove this func for release V2.0.0 +// This is for backwards compatibility with Fuji Device Services. +// If --registry= is used then we must use the for the configuration provider. +// GetRegistryProviderInfoOverride get the overrides for Registry Provider Config values +// from an Environment variable value (if it exists). +func (_ *Environment) GetRegistryProviderInfoOverride(lc logger.LoggingClient) string { + url := os.Getenv(envKeyRegistryUrl) + if len(url) > 0 { + lc.Info(fmt.Sprintf("Registry Provider information overridden by Environment variable: %s=%s", envKeyRegistryUrl, url)) + } + + return url +} + +// OverrideStartupInfo overrides the Service StartupInfo values from an Environment variable value (if it exists). +func (_ *Environment) OverrideStartupInfo( lc logger.LoggingClient, startup config.StartupInfo) config.StartupInfo { - // Override the startup timer configuration, if provided. - if env := os.Getenv(envKeyStartupDuration); env != "" { - lc.Info(fmt.Sprintf("Overriding startup duration from environment variable. %s=%s", envKeyStartupDuration, env)) + // OverrideConfiguration the startup timer configuration, if provided. + if value := os.Getenv(envKeyStartupDuration); len(value) > 0 { + lc.Info(fmt.Sprintf("Startup duration value overridden by Environment variable: %s=%s", envKeyStartupDuration, value)) - if n, err := strconv.ParseInt(env, 10, 0); err == nil && n > 0 { + if n, err := strconv.ParseInt(value, 10, 0); err == nil && n > 0 { startup.Duration = int(n) } } - // Override the startup timer interval, if provided. - if env := os.Getenv(envKeyStartupInterval); env != "" { - lc.Info(fmt.Sprintf("Overriding startup interval from environment variable. %s=%s", envKeyStartupInterval, env)) + // OverrideConfiguration the startup timer interval, if provided. + if value := os.Getenv(envKeyStartupInterval); len(value) > 0 { + lc.Info(fmt.Sprintf("Startup interval value overridden by Environment variable: %s=%s", envKeyStartupInterval, value)) - if n, err := strconv.ParseInt(env, 10, 0); err == nil && n > 0 { + if n, err := strconv.ParseInt(value, 10, 0); err == nil && n > 0 { startup.Interval = int(n) } } return startup } + +// convertToType attempts to convert the string value to the specified type of the old value +func (_ *Environment) 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 +} + +// 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 +} diff --git a/bootstrap/config/environment_test.go b/bootstrap/config/environment_test.go index 3ac0111f..f6102483 100644 --- a/bootstrap/config/environment_test.go +++ b/bootstrap/config/environment_test.go @@ -15,9 +15,13 @@ package config import ( + "fmt" "os" + "strconv" "testing" + "github.com/stretchr/testify/require" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" "github.com/edgexfoundry/go-mod-bootstrap/config" @@ -29,8 +33,10 @@ import ( ) const ( - goodUrlValue = "consul.http://localhost:8500" - badUrlValue = "Not a url" + goodUrlValue = "consul.http://localhost:8500" + goodRegistryUrlValue = "consul://localhost:8500" + + badUrlValue = "Not a url" expectedTypeValue = "consul" expectedHostValue = "localhost" @@ -66,14 +72,14 @@ func initializeTest() (types.ServiceConfig, config.StartupInfo, logger.LoggingCl return providerConfig, startupInfo, logging.FactoryToStdout("unit-test") } -func TestEnvVariableUpdatesConfigProviderInfo(t *testing.T) { +func TestOverrideConfigProviderInfo(t *testing.T) { providerConfig, _, lc := initializeTest() - if err := os.Setenv(envKeyUrl, goodUrlValue); err != nil { - t.Fail() - } + err := os.Setenv(envKeyConfigUrl, goodUrlValue) + require.NoError(t, err) - providerConfig, err := OverrideConfigProviderInfoFromEnvironment(lc, providerConfig) + env := NewEnvironment() + providerConfig, err = env.OverrideConfigProviderInfo(lc, providerConfig) assert.NoError(t, err, "Unexpected error") assert.Equal(t, providerConfig.Host, expectedHostValue) @@ -82,10 +88,28 @@ func TestEnvVariableUpdatesConfigProviderInfo(t *testing.T) { assert.Equal(t, providerConfig.Protocol, expectedProtocolValue) } -func TestNoEnvVariableDoesNotUpdateConfigProviderInfo(t *testing.T) { +// TODO: Remove once -registry is back to a bool in release V2.0.0 +func TestOverrideConfigProviderInfo_RegistryUrl(t *testing.T) { providerConfig, _, lc := initializeTest() - providerConfig, err := OverrideConfigProviderInfoFromEnvironment(lc, providerConfig) + err := os.Setenv(envKeyRegistryUrl, goodRegistryUrlValue) + require.NoError(t, err) + + env := NewEnvironment() + providerConfig, err = env.OverrideConfigProviderInfo(lc, providerConfig) + + require.NoError(t, err, "Unexpected error") + assert.Equal(t, providerConfig.Host, expectedHostValue) + assert.Equal(t, providerConfig.Port, expectedPortValue) + assert.Equal(t, providerConfig.Type, expectedTypeValue) + assert.Equal(t, providerConfig.Protocol, expectedProtocolValue) +} + +func TestOverrideConfigProviderInfo_NoEnvVariables(t *testing.T) { + providerConfig, _, lc := initializeTest() + + env := NewEnvironment() + providerConfig, err := env.OverrideConfigProviderInfo(lc, providerConfig) assert.NoError(t, err, "Unexpected error") assert.Equal(t, providerConfig.Host, defaultHostValue) @@ -94,39 +118,146 @@ func TestNoEnvVariableDoesNotUpdateConfigProviderInfo(t *testing.T) { assert.Equal(t, providerConfig.Protocol, defaultProtocolValue) } -func TestEnvVariableUpdateConfigProviderInfoError(t *testing.T) { +func TestOverrideConfigProviderInfo_ConfigProviderInfoError(t *testing.T) { providerConfig, _, lc := initializeTest() - if err := os.Setenv(envKeyUrl, badUrlValue); err != nil { - t.Fail() - } + err := os.Setenv(envKeyConfigUrl, badUrlValue) + require.NoError(t, err) - _, err := OverrideConfigProviderInfoFromEnvironment(lc, providerConfig) + env := NewEnvironment() + _, err = env.OverrideConfigProviderInfo(lc, providerConfig) assert.Error(t, err, "Expected an error") } -func TestEnvVariableUpdatesStartupInfo(t *testing.T) { +// TODO: Remove once -registry is back to a bool in release V2.0.0 +func TestGetRegistryProviderInfoOverride(t *testing.T) { + _, _, lc := initializeTest() + + err := os.Setenv(envKeyRegistryUrl, goodRegistryUrlValue) + require.NoError(t, err) + env := NewEnvironment() + actual := env.GetRegistryProviderInfoOverride(lc) + assert.Equal(t, goodRegistryUrlValue, actual) +} + +func TestOverrideStartupInfo(t *testing.T) { _, startupInfo, lc := initializeTest() - if err := os.Setenv(envKeyStartupDuration, envStartupDuration); err != nil { - t.Fail() - } - if err := os.Setenv(envKeyStartupInterval, envStartupInterval); err != nil { - t.Fail() - } + err := os.Setenv(envKeyStartupDuration, envStartupDuration) + require.NoError(t, err) - startupInfo = OverrideStartupInfoFromEnvironment(lc, startupInfo) + err = os.Setenv(envKeyStartupInterval, envStartupInterval) + require.NoError(t, err) + + env := NewEnvironment() + startupInfo = env.OverrideStartupInfo(lc, startupInfo) assert.Equal(t, startupInfo.Duration, expectedStartupDuration) assert.Equal(t, startupInfo.Interval, expectedStartupInterval) } -func TestNoEnvVariableDoesNotUpdateSetupInfo(t *testing.T) { +func TestOverrideStartupInfo_NoEnvVariables(t *testing.T) { _, startupInfo, lc := initializeTest() - startupInfo = OverrideStartupInfoFromEnvironment(lc, startupInfo) + env := NewEnvironment() + startupInfo = env.OverrideStartupInfo(lc, startupInfo) assert.Equal(t, startupInfo.Duration, defaultStartupDuration) assert.Equal(t, startupInfo.Interval, defaultStartupInterval) } + +func TestConvertToType(t *testing.T) { + tests := []struct { + Name string + Value string + OldValue interface{} + ExpectedValue interface{} + ExpectedError string + }{ + {Name: "String", Value: "This is string", OldValue: "string", ExpectedValue: "This is string"}, + {Name: "Valid String slice", Value: " val1 , val2 ", OldValue: []string{}, ExpectedValue: []interface{}{"val1", "val2"}}, + {Name: "Invalid slice type", Value: "", OldValue: []int{}, ExpectedError: "'[]int' is not supported"}, + {Name: "Valid bool", Value: "true", OldValue: true, ExpectedValue: true}, + {Name: "Invalid bool", Value: "bad bool", OldValue: false, ExpectedError: "invalid syntax"}, + {Name: "Valid int", Value: "234", OldValue: 0, ExpectedValue: 234}, + {Name: "Invalid int", Value: "one", OldValue: 0, ExpectedError: "invalid syntax"}, + {Name: "Valid int8", Value: "123", OldValue: int8(0), ExpectedValue: int8(123)}, + {Name: "Invalid int8", Value: "897", OldValue: int8(0), ExpectedError: "value out of range"}, + {Name: "Valid int16", Value: "897", OldValue: int16(0), ExpectedValue: int16(897)}, + {Name: "Invalid int16", Value: "89756789", OldValue: int16(0), ExpectedError: "value out of range"}, + {Name: "Valid int32", Value: "89756789", OldValue: int32(0), ExpectedValue: int32(89756789)}, + {Name: "Invalid int32", Value: "89756789324414221", OldValue: int32(0), ExpectedError: "value out of range"}, + {Name: "Valid int64", Value: "89756789324414221", OldValue: int64(0), ExpectedValue: int64(89756789324414221)}, + {Name: "Invalid int64", Value: "one", OldValue: int64(0), ExpectedError: "invalid syntax"}, + {Name: "Valid uint", Value: "234", OldValue: uint(0), ExpectedValue: uint(234)}, + {Name: "Invalid uint", Value: "one", OldValue: uint(0), ExpectedError: "invalid syntax"}, + {Name: "Valid uint8", Value: "123", OldValue: uint8(0), ExpectedValue: uint8(123)}, + {Name: "Invalid uint8", Value: "897", OldValue: uint8(0), ExpectedError: "value out of range"}, + {Name: "Valid uint16", Value: "897", OldValue: uint16(0), ExpectedValue: uint16(897)}, + {Name: "Invalid uint16", Value: "89756789", OldValue: uint16(0), ExpectedError: "value out of range"}, + {Name: "Valid uint32", Value: "89756789", OldValue: uint32(0), ExpectedValue: uint32(89756789)}, + {Name: "Invalid uint32", Value: "89756789324414221", OldValue: uint32(0), ExpectedError: "value out of range"}, + {Name: "Valid uint64", Value: "89756789324414221", OldValue: uint64(0), ExpectedValue: uint64(89756789324414221)}, + {Name: "Invalid uint64", Value: "one", OldValue: uint64(0), ExpectedError: "invalid syntax"}, + {Name: "Valid float32", Value: "895.89", OldValue: float32(0), ExpectedValue: float32(895.89)}, + {Name: "Invalid float32", Value: "one", OldValue: float32(0), ExpectedError: "invalid syntax"}, + {Name: "Valid float64", Value: "89756789324414221.5689", OldValue: float64(0), ExpectedValue: 89756789324414221.5689}, + {Name: "Invalid float64", Value: "one", OldValue: float64(0), ExpectedError: "invalid syntax"}, + {Name: "Invalid Value Type", Value: "anything", OldValue: make(chan int), ExpectedError: "type of 'chan int' is not supported"}, + } + + env := Environment{} + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + actual, err := env.convertToType(test.OldValue, test.Value) + if len(test.ExpectedError) > 0 { + require.Error(t, err) + assert.Contains(t, err.Error(), test.ExpectedError) + return // test complete + } + + require.NoError(t, err) + assert.Equal(t, test.ExpectedValue, actual) + }) + } +} + +func TestOverrideConfiguration(t *testing.T) { + _, _, lc := initializeTest() + + serviceConfig := struct { + Registry config.RegistryInfo + List []string + FloatVal float32 + }{ + Registry: config.RegistryInfo{ + Host: "localhost", + Port: 8500, + Type: "consul", + }, + List: []string{"val1"}, + FloatVal: float32(11.11), + } + + expectedOverrideCount := 4 + expectedHost := "edgex-core-consul" + expectedPort := 98500 + expectedList := []string{"joe", "mary", "bob"} + expectedFloatVal := float32(24.234) + _ = os.Setenv("Registry_Host", expectedHost) + _ = os.Setenv("Registry_Port", strconv.Itoa(expectedPort)) + _ = os.Setenv("List", " joe,mary , bob ") + strval := fmt.Sprintf("%v", expectedFloatVal) + _ = os.Setenv("FloatVal", strval) + + env := NewEnvironment() + actualCount, err := env.OverrideConfiguration(lc, &serviceConfig) + + require.NoError(t, err) + assert.Equal(t, expectedOverrideCount, actualCount) + assert.Equal(t, expectedHost, serviceConfig.Registry.Host) + assert.Equal(t, expectedPort, serviceConfig.Registry.Port) + assert.Equal(t, expectedList, serviceConfig.List) + assert.Equal(t, expectedFloatVal, serviceConfig.FloatVal) +} diff --git a/bootstrap/config/file.go b/bootstrap/config/file.go index 821a8c02..6062faba 100644 --- a/bootstrap/config/file.go +++ b/bootstrap/config/file.go @@ -19,11 +19,16 @@ import ( "io/ioutil" "os" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" - + "github.com/BurntSushi/toml" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - "github.com/BurntSushi/toml" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" +) + +const ( + envConfDir = "EDGEX_CONF_DIR" + envProfile = "edgex_profile" // TODO: change to EDGEX_PROFILE for release v2.0.0 + envFile = "EDGEX_CONFIG_FILE" ) // LoadFromFile attempts to read and unmarshal toml-based configuration into a configuration struct. @@ -35,18 +40,33 @@ func LoadFromFile( config interfaces.Configuration) error { // ported from determinePath() in internal/pkg/config/loader.go - if len(configDir) == 0 { - configDir = os.Getenv("EDGEX_CONF_DIR") + envValue := os.Getenv(envConfDir) + if len(envValue) > 0 { + configDir = envValue + lc.Info(fmt.Sprintf("Environment varable override of -confdir value by Environment varable: %s=%s", envConfDir, envValue)) } + if len(configDir) == 0 { configDir = "./res" } + envValue = os.Getenv(envProfile) + if len(envValue) > 0 { + profileDir = envValue + lc.Info(fmt.Sprintf("Environment varable override of -profile value by Environment varable: %s=%s", envProfile, envValue)) + } + // remainder is simplification of LoadFromFile() in internal/pkg/config/loader.go if len(profileDir) > 0 { profileDir += "/" } + envValue = os.Getenv(envFile) + if len(envValue) > 0 { + configFileName = envValue + lc.Info(fmt.Sprintf("Environment varable override of -file value overridden by Environment varable: %s=%s", envFile, envValue)) + } + fileName := configDir + "/" + profileDir + configFileName contents, err := ioutil.ReadFile(fileName) diff --git a/bootstrap/config/provider.go b/bootstrap/config/provider.go index dd36e20c..e0b468f9 100644 --- a/bootstrap/config/provider.go +++ b/bootstrap/config/provider.go @@ -15,7 +15,6 @@ package config import ( "github.com/edgexfoundry/go-mod-configuration/pkg/types" - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" ) @@ -25,7 +24,7 @@ type ProviderInfo struct { } // NewProviderInfo creates a new ProviderInfo and initializes it -func NewProviderInfo(lc logger.LoggingClient, providerUrl string) (*ProviderInfo, error) { +func NewProviderInfo(lc logger.LoggingClient, environment *Environment, providerUrl string) (*ProviderInfo, error) { var err error configProviderInfo := ProviderInfo{} @@ -36,8 +35,8 @@ func NewProviderInfo(lc logger.LoggingClient, providerUrl string) (*ProviderInfo } } - // override file-based configuration with environment variables. - configProviderInfo.serviceConfig, err = OverrideConfigProviderInfoFromEnvironment(lc, configProviderInfo.serviceConfig) + // override file-based configuration with Environment variables. + configProviderInfo.serviceConfig, err = environment.OverrideConfigProviderInfo(lc, configProviderInfo.serviceConfig) if err != nil { return nil, err } diff --git a/bootstrap/config/provider_test.go b/bootstrap/config/provider_test.go index c84f6e12..ac8da7c8 100644 --- a/bootstrap/config/provider_test.go +++ b/bootstrap/config/provider_test.go @@ -17,15 +17,16 @@ import ( "os" "testing" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" - "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" ) func TestNewConfigProviderInfoUrl(t *testing.T) { lc := logging.FactoryToStdout("unit-test") - target, err := NewProviderInfo(lc, goodUrlValue) + env := NewEnvironment() + target, err := NewProviderInfo(lc, env, goodUrlValue) if !assert.NoError(t, err, "unexpected error") { t.Fatal() } @@ -41,11 +42,12 @@ func TestNewConfigProviderInfoUrl(t *testing.T) { func TestNewConfigProviderInfoEnv(t *testing.T) { lc := logging.FactoryToStdout("unit-test") - if err := os.Setenv(envKeyUrl, goodUrlValue); err != nil { + if err := os.Setenv(envKeyConfigUrl, goodUrlValue); err != nil { t.Fail() } - target, err := NewProviderInfo(lc, goodUrlValue) + env := NewEnvironment() + target, err := NewProviderInfo(lc, env, goodUrlValue) if !assert.NoError(t, err, "unexpected error") { t.Fatal() } @@ -61,7 +63,8 @@ func TestNewConfigProviderInfoEnv(t *testing.T) { func TestNewConfigProviderInfoBadUrl(t *testing.T) { lc := logging.FactoryToStdout("unit-test") - _, err := NewProviderInfo(lc, badUrlValue) + env := NewEnvironment() + _, err := NewProviderInfo(lc, env, badUrlValue) if !assert.Error(t, err, "Expected an error") { t.Fatal() } @@ -70,11 +73,12 @@ func TestNewConfigProviderInfoBadUrl(t *testing.T) { func TestNewConfigProviderInfoBadEnvUrl(t *testing.T) { lc := logging.FactoryToStdout("unit-test") - if err := os.Setenv(envKeyUrl, badUrlValue); err != nil { + if err := os.Setenv(envKeyConfigUrl, badUrlValue); err != nil { t.Fail() } - _, err := NewProviderInfo(lc, goodUrlValue) + env := NewEnvironment() + _, err := NewProviderInfo(lc, env, goodUrlValue) if !assert.Error(t, err, "Expected an error") { t.Fatal() } diff --git a/bootstrap/flags/flags.go b/bootstrap/flags/flags.go index 129a6a76..fdb7bd7c 100644 --- a/bootstrap/flags/flags.go +++ b/bootstrap/flags/flags.go @@ -23,12 +23,20 @@ import ( const ( DefaultConfigProvider = "consul.http://localhost:8500" - DefaultConfigFile = "configuration.toml" + + // TODO: Remove these for release v2.0.0 when -registry is a bool + UseRegistryNoUrlValue = "." + DontUseRegistryValue = "~" + + DefaultConfigFile = "configuration.toml" ) // Common is an interface that defines AP for the common command-line flags used by most EdgeX services type Common interface { + OverwriteConfig() bool UseRegistry() bool + // TODO: Remove for release v2.0.0 once --registry= no longer supported + RegistryUrl() string ConfigProviderUrl() string Profile() string ConfigDirectory() string @@ -41,7 +49,8 @@ type Common interface { type Default struct { FlagSet *flag.FlagSet additionalUsage string - useRegistry bool + overwriteConfig bool + registry string // TODO: Change back to `useRegistry bool` for release v2.0.0 configProviderUrl string profile string configDir string @@ -66,24 +75,39 @@ func (d *Default) Parse(arguments []string) { // The flags package doesn't allow for String flags to be specified without a value, so to support // -cp/-configProvider without value to indicate using default host value we must detect use of this option with // out value and insert the default value before parsing the command line options. + // for index, option := range arguments { if strings.Contains(option, "-cp") || strings.Contains(option, "-configProvider") { if !strings.Contains(option, "=") { arguments[index] = "-cp=" + DefaultConfigProvider } + // TODO: Remove this for release v2.0.0 when --registry is a bool + // For backwards compatibility with Fuji Device Services, -r/-registry can contain a provider URL. + // It can also be used as a bool, i.e. not value, but flags doesn't all ow no value so we have to detect this + // and give it a value that represent "no value' + } else if strings.Contains(option, "-r") || strings.Contains(option, "-registry") { + if !strings.Contains(option, "=") { + arguments[index] = "-r=" + UseRegistryNoUrlValue + } } } // Usage is provided by caller, so leaving individual usage blank here so not confusing where if comes from. d.FlagSet.StringVar(&d.configProviderUrl, "configProvider", "", "") d.FlagSet.StringVar(&d.configProviderUrl, "cp", "", "") + d.FlagSet.BoolVar(&d.overwriteConfig, "overwrite", false, "") + d.FlagSet.BoolVar(&d.overwriteConfig, "o", false, "") d.FlagSet.StringVar(&d.configFileName, "f", DefaultConfigFile, "") d.FlagSet.StringVar(&d.configFileName, "file", DefaultConfigFile, "") d.FlagSet.StringVar(&d.profile, "profile", "", "") d.FlagSet.StringVar(&d.profile, "p", "", ".") d.FlagSet.StringVar(&d.configDir, "confdir", "", "") - d.FlagSet.BoolVar(&d.useRegistry, "registry", false, "") - d.FlagSet.BoolVar(&d.useRegistry, "r", false, "") + d.FlagSet.StringVar(&d.configDir, "c", "", "") + + // TODO: Change registry back to being a boolean for release v2.0.0 + // For backwards compatibility with Fuji Device Service, registry is a string that can contain a provider URL. + d.FlagSet.StringVar(&d.registry, "registry", DontUseRegistryValue, "") + d.FlagSet.StringVar(&d.registry, "r", DontUseRegistryValue, "") d.FlagSet.Usage = d.helpCallback @@ -94,9 +118,28 @@ func (d *Default) Parse(arguments []string) { } } +// OverwriteConfig returns whether the local configuration should be pushed (overwrite) into the Configuration provider +func (d *Default) OverwriteConfig() bool { + return d.overwriteConfig +} + // UseRegistry returns whether the Registry should be used or not func (d *Default) UseRegistry() bool { - return d.useRegistry + // TODO: Change -registry back to being a boolean for release v2.0.0 + // For backwards compatibility with Fuji Device Service, registry is a string that can contain a provider URL. + return d.registry != DontUseRegistryValue +} + +// TODO: Remove this interface for release v2.0.0 +// RegistryUrl returns the url for the Registry Provider, if one was specified. +// For backwards compatibility with Fuji Device Service, registry is a string that can contain a provider URL. +func (d *Default) RegistryUrl() string { + // 1 accounts for the default values that are not urls + if len(d.registry) > 1 { + return d.registry + } + + return "" } // ConfigProviderUrl returns the url for the Configuration Provider, if one was specified. @@ -126,15 +169,21 @@ func (d *Default) Help() { // commonHelpCallback displays the help usage message and exits func (d *Default) helpCallback() { + // TODO: adjust -r/--registry description for release v2.0.0 when it is a bool fmt.Printf( "Usage: %s [options]\n"+ "Server Options:\n"+ " -cp, --configProvider Indicates to use Configuration Provider service at specified URL.\n"+ " URL Format: {type}.{protocol}://{host}:{port} ex: consul.http://localhost:8500\n"+ + " -o, --overwrite Overwrite configuration in provider with local configuration\n"+ + " *** Use with cation *** Use will clobber existing settings in provider,\n"+ + " problematic if those settings were edited by hand intentionally\n"+ " -f, --file Indicates name of the local configuration file. Defaults to configuration.toml\n"+ " -p, --profile Indicate configuration profile other than default\n"+ - " --confdir Specify local configuration directory\n"+ - " -r, --registry Indicates service should use Registry\n"+ + " -c, --confdir Specify local configuration directory\n"+ + " -r, --registry Indicates service should use Registry.\n"+ + " Legacy Device Services may specify provider URL with the following format:\n"+ + " URL Format: {type}://{host}:{port} ex: consul://localhost:8500\n"+ "%s\n"+ "Common Options:\n"+ " -h, --help Show this message\n", diff --git a/bootstrap/flags/flags_test.go b/bootstrap/flags/flags_test.go index d3aea19a..ad1d7bef 100644 --- a/bootstrap/flags/flags_test.go +++ b/bootstrap/flags/flags_test.go @@ -34,14 +34,16 @@ func TestNewAllFlags(t *testing.T) { actual := newSUT( []string{ + "-o", "-r", "-p=" + expectedProfile, - "-confdir=" + expectedConfigDirectory, + "-c=" + expectedConfigDirectory, "-f=" + expectedFileName, }, ) - assert.Equal(t, true, actual.UseRegistry()) + assert.Equal(t, true, actual.OverwriteConfig()) + assert.True(t, actual.UseRegistry()) assert.Equal(t, expectedProfile, actual.Profile()) assert.Equal(t, expectedConfigDirectory, actual.ConfigDirectory()) assert.Equal(t, expectedFileName, actual.ConfigFileName()) @@ -50,7 +52,8 @@ func TestNewAllFlags(t *testing.T) { func TestNewDefaultsNoFlags(t *testing.T) { actual := newSUT([]string{}) - assert.Equal(t, false, actual.UseRegistry()) + assert.Equal(t, false, actual.OverwriteConfig()) + assert.False(t, actual.UseRegistry()) assert.Equal(t, "", actual.ConfigProviderUrl()) assert.Equal(t, "", actual.Profile()) assert.Equal(t, "", actual.ConfigDirectory()) @@ -84,3 +87,12 @@ func TestNewOverrideConfigProvider(t *testing.T) { assert.Equal(t, expectedConfigProviderUrl, actual.ConfigProviderUrl()) } + +// TODO: Remove for release v2.0.0 when -registry is a bool +func TestNewOverrideRegistry(t *testing.T) { + expectedRegistryUrl := "consul://docker-core-consul:8500" + + actual := newSUT([]string{"-registry=" + expectedRegistryUrl}) + + assert.Equal(t, expectedRegistryUrl, actual.RegistryUrl()) +} diff --git a/bootstrap/registration/registry.go b/bootstrap/registration/registry.go index 5a69282d..7e2868f2 100644 --- a/bootstrap/registration/registry.go +++ b/bootstrap/registration/registry.go @@ -18,22 +18,33 @@ import ( "context" "errors" "fmt" + "net/url" + "strconv" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" - + "github.com/edgexfoundry/go-mod-configuration/pkg/types" "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - registryTypes "github.com/edgexfoundry/go-mod-registry/pkg/types" "github.com/edgexfoundry/go-mod-registry/registry" -) -const () + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/flags" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" +) // createRegistryClient creates and returns a registry.Client instance. -func createRegistryClient(serviceKey string, config interfaces.Configuration, lc logger.LoggingClient) (registry.Client, error) { - bootstrapConfig := config.GetBootstrap() +// For backwards compatibility with Fuji Device Service, -registry is a string that can contain a provider URL. +// TODO: Remove registryUrl parameter for release v2.0.0 +func createRegistryClient( + serviceKey string, + serviceConfig interfaces.Configuration, + registryUrl string, + environment *config.Environment, + lc logger.LoggingClient) (registry.Client, error) { + var err error + bootstrapConfig := serviceConfig.GetBootstrap() + registryConfig := registryTypes.Config{ Host: bootstrapConfig.Registry.Host, Port: bootstrapConfig.Registry.Port, @@ -46,16 +57,63 @@ func createRegistryClient(serviceKey string, config interfaces.Configuration, lc CheckRoute: clients.ApiPingRoute, } - lc.Info(fmt.Sprintf("Using Registry from %s", registryConfig.GetRegistryUrl())) + // TODO: Remove this block for release v2.0.0 + // For backwards compatibility with Fuji Device Service, -registry is a string that can contain a provider URL. + if len(registryUrl) > 0 && registryUrl != flags.UseRegistryNoUrlValue { + registryConfig, err = OverrideRegistryConfigWithUrl(registryConfig, registryUrl) + if err != nil { + return nil, err + } + } + + // TODO: Remove this block for release v2.0.0 + // For backwards compatibility, registry information can be override with environment variable. + registryUrl = environment.GetRegistryProviderInfoOverride(lc) + if len(registryUrl) > 0 { + registryConfig, err = OverrideRegistryConfigWithUrl(registryConfig, registryUrl) + if err != nil { + return nil, err + } + } + + lc.Info(fmt.Sprintf("Using Registry (%s) from %s", registryConfig.Type, registryConfig.GetRegistryUrl())) return registry.NewRegistryClient(registryConfig) } +// TODO: Remove this func for release v2.0.0 +// For backwards compatibility with Fuji Device Service, -registry is a string that can contain a provider URL. +func OverrideRegistryConfigWithUrl(registryConfig registryTypes.Config, registryUrl string) (registryTypes.Config, error) { + if len(registryUrl) == 0 { + return registryConfig, nil + } + + urlDetails, err := url.Parse(registryUrl) + if err != nil { + return registryConfig, fmt.Errorf("failed to parse Registry Provider URL (%s): %s", registryUrl, err.Error()) + } + + port, err := strconv.Atoi(urlDetails.Port()) + if err != nil { + return registryConfig, fmt.Errorf("failed to parse Registry Provider URL (%s): %s", registryUrl, err.Error()) + } + + registryConfig.Port = port + registryConfig.Host = urlDetails.Hostname() + registryConfig.Protocol = types.DefaultProtocol + registryConfig.Type = urlDetails.Scheme + + return registryConfig, nil +} + +// TODO: Remove registryUrl parameter for release v2.0.0 // RegisterWithRegistry connects to the registry and registers the service with the Registry func RegisterWithRegistry( ctx context.Context, startupTimer startup.Timer, config interfaces.Configuration, + registryUrl string, + environment *config.Environment, lc logger.LoggingClient, serviceKey string) (registry.Client, error) { @@ -71,7 +129,7 @@ func RegisterWithRegistry( return nil } - registryClient, err := createRegistryClient(serviceKey, config, lc) + registryClient, err := createRegistryClient(serviceKey, config, registryUrl, environment, lc) if err != nil { return nil, fmt.Errorf("createRegistryClient failed: %v", err.Error()) } diff --git a/bootstrap/registration/registry_test.go b/bootstrap/registration/registry_test.go new file mode 100644 index 00000000..28ace48a --- /dev/null +++ b/bootstrap/registration/registry_test.go @@ -0,0 +1,169 @@ +// +// Copyright (c) 2019 Intel Corporation +// +// 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 registration + +import ( + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/edgexfoundry/go-mod-registry/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + envConfig "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/config" +) + +// TODO: Remove RegistryUrl parts for release v2.0.0 when -registry is a bool +func TestCreateRegistryClient(t *testing.T) { + lc := logger.NewClientStdOut("unit-test", false, "TRACE") + tests := []struct { + Name string + RegistryUrl string + ExpectedError string + }{ + { + Name: "Success - blank url", + RegistryUrl: "", + }, + { + Name: "Success - with url", + RegistryUrl: "consul://localhost:8500", + }, + { + Name: "Success - with dot url", + RegistryUrl: ".", + }, + { + Name: "Failure - bad url", + RegistryUrl: "not a url", + ExpectedError: "failed to parse Registry Provider URL (not a url):", + }, + { + Name: "Failure - RegistryUrl Missing port", + RegistryUrl: "consul://localhost", + ExpectedError: "failed to parse Registry Provider URL (consul://localhost): strconv.Atoi:", + }, + } + + serviceConfig := unitTestConfiguration{ + Service: config.ServiceInfo{ + Host: "localhost", + Port: 8080, + Protocol: "http", + }, + Registry: config.RegistryInfo{ + Host: "localhost", + Port: 8500, + Type: "consul", + }, + } + + env := envConfig.NewEnvironment() + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + actual, err := createRegistryClient("unit-test", serviceConfig, test.RegistryUrl, env, lc) + if len(test.ExpectedError) > 0 { + require.Error(t, err) + assert.Contains(t, err.Error(), test.ExpectedError) + return // test complete + } + + require.NoError(t, err) + assert.NotNil(t, actual) + }) + } +} + +// TODO: Remove this test for release v2.0.0 when -registry is a bool +func TestOverrideRegistryConfigWithUrl(t *testing.T) { + + tests := []struct { + Name string + RegistryUrl string + Expected types.Config + ExpectedError string + }{ + { + Name: "Success - Good URL", + RegistryUrl: "consul://localhost:8500", + Expected: types.Config{ + Type: "consul", + Protocol: "http", + Host: "localhost", + Port: 8500, + }, + }, + { + Name: "Error - Bad URL", + RegistryUrl: "not a url", + ExpectedError: "failed to parse Registry Provider URL (not a url):", + }, + { + Name: "Error - RegistryUrl Missing port", + RegistryUrl: "consul://localhost", + ExpectedError: "failed to parse Registry Provider URL (consul://localhost): strconv.Atoi:", + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + actual, err := OverrideRegistryConfigWithUrl(types.Config{}, test.RegistryUrl) + if len(test.ExpectedError) > 0 { + require.Error(t, err) + assert.Contains(t, err.Error(), test.ExpectedError) + return // test complete + } + + require.NoError(t, err) + assert.Equal(t, test.Expected, actual) + }) + } +} + +type unitTestConfiguration struct { + Service config.ServiceInfo + Registry config.RegistryInfo +} + +func (ut unitTestConfiguration) UpdateFromRaw(rawConfig interface{}) bool { + panic("should not be called") +} + +func (ut unitTestConfiguration) EmptyWritablePtr() interface{} { + panic("should not be called") +} + +func (ut unitTestConfiguration) UpdateWritableFromRaw(rawWritable interface{}) bool { + panic("should not be called") +} + +func (ut unitTestConfiguration) GetBootstrap() interfaces.BootstrapConfiguration { + return interfaces.BootstrapConfiguration{ + Service: ut.Service, + Registry: ut.Registry, + } +} + +func (ut unitTestConfiguration) GetLogLevel() string { + return "TRACE" +} + +func (ut unitTestConfiguration) GetRegistryInfo() config.RegistryInfo { + return ut.Registry +} diff --git a/go.mod b/go.mod index e055199f..40da64b5 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,13 @@ module github.com/edgexfoundry/go-mod-bootstrap require ( github.com/BurntSushi/toml v0.3.1 - github.com/edgexfoundry/go-mod-configuration v0.0.0 + github.com/edgexfoundry/go-mod-configuration v0.0.3 github.com/edgexfoundry/go-mod-core-contracts v0.1.34 github.com/edgexfoundry/go-mod-registry v0.1.17 github.com/edgexfoundry/go-mod-secrets v0.0.17 github.com/gorilla/mux v1.7.1 - github.com/stretchr/testify v1.3.0 + github.com/pelletier/go-toml v1.2.0 + github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.2.8 // indirect )