diff --git a/bootstrap/config/config.go b/bootstrap/config/config.go index 98ecfc5d..4459615d 100644 --- a/bootstrap/config/config.go +++ b/bootstrap/config/config.go @@ -60,6 +60,8 @@ const ( deviceServicesKey = "device-services" ) +var invalidRemoteHostsError = errors.New("-rsh/--remoteServiceHosts must contain 3 and only 3 comma seperated host names") + // UpdatedStream defines the stream type that is notified by ListenForChanges when a configuration update is received. type UpdatedStream chan struct{} @@ -124,6 +126,7 @@ func (cp *Processor) Process( cp.overwriteConfig = cp.flags.OverwriteConfig() configProviderUrl := cp.flags.ConfigProviderUrl() + remoteHosts := environment.GetRemoteServiceHosts(cp.lc, cp.flags.RemoteServiceHosts()) // Create new ProviderInfo and initialize it from command-line flag or Variables configProviderInfo, err := NewProviderInfo(cp.envVars, configProviderUrl) @@ -133,10 +136,30 @@ func (cp *Processor) Process( useProvider := configProviderInfo.UseProvider() + mode := &container.DevRemoteMode{ + InDevMode: cp.flags.InDevMode(), + InRemoteMode: remoteHosts != nil, + } + + cp.dic.Update(di.ServiceConstructorMap{ + container.DevRemoteModeName: func(get di.Get) interface{} { + return mode + }, + }) + var privateConfigClient configuration.Client var privateServiceConfig interfaces.Configuration if useProvider { + if remoteHosts != nil { + if len(remoteHosts) != 3 { + return invalidRemoteHostsError + } + + cp.lc.Infof("Setting config Provider host to %s", remoteHosts[1]) + configProviderInfo.SetHost(remoteHosts[1]) + } + getAccessToken, err := cp.getAccessTokenCallback(serviceKey, secretProvider, err, configProviderInfo) if err != nil { return err @@ -283,9 +306,51 @@ func (cp *Processor) Process( } } + if remoteHosts != nil { + err = applyRemoteHosts(remoteHosts, serviceConfig) + if err != nil { + return err + } + } + return err } +func applyRemoteHosts(remoteHosts []string, serviceConfig interfaces.Configuration) error { + if len(remoteHosts) != 3 { + return invalidRemoteHostsError + } + + config := serviceConfig.GetBootstrap() + + config.Service.Host = remoteHosts[0] + config.Service.ServerBindAddr = remoteHosts[2] + + if config.Config != nil { + config.Config.Host = remoteHosts[1] + } + + if config.MessageBus != nil { + config.MessageBus.Host = remoteHosts[1] + } + + if config.Registry != nil { + config.Registry.Host = remoteHosts[1] + } + + if config.Database != nil { + config.Database.Host = remoteHosts[1] + } + + if config.Clients != nil { + for _, client := range *config.Clients { + client.Host = remoteHosts[1] + } + } + + return nil +} + type createProviderCallback func( logger.LoggingClient, string, diff --git a/bootstrap/config/config_test.go b/bootstrap/config/config_test.go index 513cb708..e4f20cfc 100644 --- a/bootstrap/config/config_test.go +++ b/bootstrap/config/config_test.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2022 Intel Corp. + * Copyright 2023 Intel Corp. * * 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 @@ -490,3 +490,35 @@ func TestGetInsecureSecretDataFullPath(t *testing.T) { }) } } + +func TestProcessorApplyRemoteHosts(t *testing.T) { + mockStruct := ConfigurationMockStruct{ + Registry: config.RegistryInfo{}, + Service: config.ServiceInfo{}, + MessageBus: config.MessageBusInfo{}, + Clients: config.ClientsCollection{ + "core-metadata": {}, + }, + Database: config.Database{}, + Config: config.ConfigProviderInfo{}, + } + + localIP := "1.2.3.4" + remoteIP := "5.6.7.8" + srvBindIP := "localhost" + hosts := []string{localIP, remoteIP, srvBindIP} + err := applyRemoteHosts(hosts, &mockStruct) + require.NoError(t, err) + + assert.Equal(t, localIP, mockStruct.Service.Host) + assert.Equal(t, srvBindIP, mockStruct.Service.ServerBindAddr) + assert.Equal(t, remoteIP, mockStruct.Clients["core-metadata"].Host) + assert.Equal(t, remoteIP, mockStruct.Database.Host) + assert.Equal(t, remoteIP, mockStruct.MessageBus.Host) + assert.Equal(t, remoteIP, mockStruct.Registry.Host) + assert.Equal(t, remoteIP, mockStruct.Config.Host) + + hosts = []string{localIP, remoteIP} + err = applyRemoteHosts(hosts, &mockStruct) + require.Error(t, err) +} diff --git a/bootstrap/config/configmock_test.go b/bootstrap/config/configmock_test.go index 024331cc..c3d62d2e 100644 --- a/bootstrap/config/configmock_test.go +++ b/bootstrap/config/configmock_test.go @@ -27,9 +27,14 @@ type WritableInfo struct { } type ConfigurationMockStruct struct { - Writable WritableInfo - Registry config.RegistryInfo - Trigger TriggerInfo + Writable WritableInfo + Registry config.RegistryInfo + Service config.ServiceInfo + MessageBus config.MessageBusInfo + Clients config.ClientsCollection + Database config.Database + Config config.ConfigProviderInfo + Trigger TriggerInfo } type TriggerInfo struct { @@ -64,7 +69,12 @@ func (c *ConfigurationMockStruct) UpdateWritableFromRaw(rawWritable interface{}) func (c *ConfigurationMockStruct) GetBootstrap() config.BootstrapConfiguration { return config.BootstrapConfiguration{ - Registry: &c.Registry, + Clients: &c.Clients, + Service: &c.Service, + Config: &c.Config, + Registry: &c.Registry, + MessageBus: &c.MessageBus, + Database: &c.Database, } } diff --git a/bootstrap/config/provider.go b/bootstrap/config/provider.go index 550acd13..fc4fd924 100644 --- a/bootstrap/config/provider.go +++ b/bootstrap/config/provider.go @@ -46,11 +46,16 @@ func NewProviderInfo(envVars *environment.Variables, providerUrl string) (*Provi } // UseProvider returns whether the Configuration Provider should be used or not. -func (config ProviderInfo) UseProvider() bool { +func (config *ProviderInfo) UseProvider() bool { return config.serviceConfig.Host != "" } +// SetHost sets the host name for the Configuration Provider. +func (config *ProviderInfo) SetHost(host string) { + config.serviceConfig.Host = host +} + // ServiceConfig returns service configuration for the Configuration Provider -func (config ProviderInfo) ServiceConfig() types.ServiceConfig { +func (config *ProviderInfo) ServiceConfig() types.ServiceConfig { return config.serviceConfig } diff --git a/bootstrap/config/testdata/all-service-config.yaml b/bootstrap/config/testdata/all-service-config.yaml index 560a88d1..f417cd1f 100644 --- a/bootstrap/config/testdata/all-service-config.yaml +++ b/bootstrap/config/testdata/all-service-config.yaml @@ -46,7 +46,7 @@ all-services: Database: Host: "localhost" Port: 6379 - Timeout: 5000 + Timeout: "5s" Type: "redisdb" MessageBus: diff --git a/bootstrap/config/testdata/configuration.yaml b/bootstrap/config/testdata/configuration.yaml index 661d06ab..a927f859 100644 --- a/bootstrap/config/testdata/configuration.yaml +++ b/bootstrap/config/testdata/configuration.yaml @@ -46,7 +46,7 @@ all-services: Database: Host: "localhost" Port: 6379 - Timeout: 5000 + Timeout: "5s" Type: "redisdb" MessageBus: diff --git a/bootstrap/container/dev_remote_mode.go b/bootstrap/container/dev_remote_mode.go new file mode 100644 index 00000000..0f09ad8c --- /dev/null +++ b/bootstrap/container/dev_remote_mode.go @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright 2023 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 container + +import ( + "github.com/edgexfoundry/go-mod-bootstrap/v3/di" +) + +type DevRemoteMode struct { + InDevMode bool + InRemoteMode bool +} + +// DevRemoteModeName contains the name of the DevRemoteMode struct in the DIC. +var DevRemoteModeName = di.TypeInstanceToName((*DevRemoteMode)(nil)) + +// DevRemoteModeFrom helper function queries the DIC and returns the Dev and Remotes mode flags. +func DevRemoteModeFrom(get di.Get) DevRemoteMode { + devOrRemoteMode, ok := get(DevRemoteModeName).(*DevRemoteMode) + if !ok { + return DevRemoteMode{} + } + + return *devOrRemoteMode +} diff --git a/bootstrap/environment/variables.go b/bootstrap/environment/variables.go index 49ba6305..298c7ed3 100644 --- a/bootstrap/environment/variables.go +++ b/bootstrap/environment/variables.go @@ -37,15 +37,16 @@ const ( bootRetrySecondsDefault = 1 defaultConfigDirValue = "./res" - envKeyConfigUrl = "EDGEX_CONFIG_PROVIDER" - envKeyCommonConfig = "EDGEX_COMMON_CONFIG" - envKeyUseRegistry = "EDGEX_USE_REGISTRY" - envKeyStartupDuration = "EDGEX_STARTUP_DURATION" - envKeyStartupInterval = "EDGEX_STARTUP_INTERVAL" - envKeyConfigDir = "EDGEX_CONFIG_DIR" - envKeyProfile = "EDGEX_PROFILE" - envKeyConfigFile = "EDGEX_CONFIG_FILE" - envKeyFileURITimeout = "EDGEX_FILE_URI_TIMEOUT" + envKeyConfigUrl = "EDGEX_CONFIG_PROVIDER" + envKeyCommonConfig = "EDGEX_COMMON_CONFIG" + envKeyUseRegistry = "EDGEX_USE_REGISTRY" + envKeyStartupDuration = "EDGEX_STARTUP_DURATION" + envKeyStartupInterval = "EDGEX_STARTUP_INTERVAL" + envKeyConfigDir = "EDGEX_CONFIG_DIR" + envKeyProfile = "EDGEX_PROFILE" + envKeyConfigFile = "EDGEX_CONFIG_FILE" + envKeyFileURITimeout = "EDGEX_FILE_URI_TIMEOUT" + envKeyRemoteServiceHosts = "EDGEX_REMOTE_SERVICE_HOSTS" noConfigProviderValue = "none" @@ -478,3 +479,15 @@ func GetURIRequestTimeout(lc logger.LoggingClient) time.Duration { lc.Infof("Variables override of 'URI Request Timeout' by environment variable: %s=%s", envKeyFileURITimeout, envValue) return requestTimeout } + +// GetRemoteServiceHosts gets the Remote Service host name list from an environment variable (if it exists), if not returns the passed in (default) value +func GetRemoteServiceHosts(lc logger.LoggingClient, remoteHosts []string) []string { + envValue := os.Getenv(envKeyRemoteServiceHosts) + if len(envValue) <= 0 { + return remoteHosts + } + + logEnvironmentOverride(lc, "-rsh/--remoteServiceHosts", envKeyRemoteServiceHosts, envValue) + + return strings.Split(envValue, ",") +} diff --git a/bootstrap/environment/variables_test.go b/bootstrap/environment/variables_test.go index 03c74081..a3eeab1b 100644 --- a/bootstrap/environment/variables_test.go +++ b/bootstrap/environment/variables_test.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2020 Intel Inc. + * Copyright 2023 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 @@ -180,6 +180,36 @@ func TestGetConfigDir(t *testing.T) { } } +func TestGetRemoteServiceHosts(t *testing.T) { + _, lc := initializeTest() + + testCases := []struct { + TestName string + EnvName string + EnvValue string + PassedIn []string + Expected []string + }{ + {"With Env Var", envKeyRemoteServiceHosts, "1,2,3", nil, []string{"1", "2", "3"}}, + {"With No Env Var and passed in", "", "", []string{"1", "2", "3"}, []string{"1", "2", "3"}}, + {"With No Env Var and no passed in", "", "", nil, nil}, + } + + for _, test := range testCases { + t.Run(test.TestName, func(t *testing.T) { + os.Clearenv() + + if len(test.EnvName) > 0 { + err := os.Setenv(test.EnvName, test.EnvValue) + require.NoError(t, err) + } + + actual := GetRemoteServiceHosts(lc, test.PassedIn) + assert.Equal(t, test.Expected, actual) + }) + } +} + func TestGetProfileDir(t *testing.T) { _, lc := initializeTest() diff --git a/bootstrap/flags/flags.go b/bootstrap/flags/flags.go index 24665807..7dffb179 100644 --- a/bootstrap/flags/flags.go +++ b/bootstrap/flags/flags.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "regexp" + "strings" ) const ( @@ -37,21 +38,23 @@ type Common interface { ConfigFileName() string CommonConfig() string Parse([]string) + RemoteServiceHosts() []string Help() } // Default is the Default implementation of Common used by most EdgeX services type Default struct { - FlagSet *flag.FlagSet - additionalUsage string - overwriteConfig bool - useRegistry bool - devMode bool - configProviderUrl string - commonConfig string - profile string - configDir string - configFileName string + FlagSet *flag.FlagSet + additionalUsage string + overwriteConfig bool + useRegistry bool + devMode bool + configProviderUrl string + commonConfig string + profile string + configDir string + configFileName string + remoteServiceHosts string } // NewWithUsage returns a Default struct. @@ -97,6 +100,8 @@ func (d *Default) Parse(arguments []string) { d.FlagSet.StringVar(&d.profile, "p", "", ".") d.FlagSet.StringVar(&d.configDir, "configDir", "", "") d.FlagSet.StringVar(&d.configDir, "cd", "", "") + d.FlagSet.StringVar(&d.remoteServiceHosts, "remoteServiceHosts", "", "") + d.FlagSet.StringVar(&d.remoteServiceHosts, "rsh", "", "") d.FlagSet.BoolVar(&d.useRegistry, "registry", false, "") d.FlagSet.BoolVar(&d.useRegistry, "r", false, "") d.FlagSet.BoolVar(&d.devMode, "dev", false, "") @@ -151,6 +156,14 @@ func (d *Default) CommonConfig() string { return d.commonConfig } +func (d *Default) RemoteServiceHosts() []string { + if len(d.remoteServiceHosts) == 0 { + return nil + } + + return strings.Split(d.remoteServiceHosts, ",") +} + // Help displays the usage help message and exit. func (d *Default) Help() { d.helpCallback() @@ -161,22 +174,28 @@ func (d *Default) helpCallback() { 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"+ - " -cc, --commonConfig Takes the location where the common configuration is loaded from when\n"+ - " not using the Configuration Provider\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"+ - " -cf, --configFile Indicates name of the local configuration file. Defaults to configuration.toml\n"+ - " -p, --profile Indicate configuration profile other than default\n"+ - " -cd, --configDir Specify local configuration directory\n"+ - " -r, --registry Indicates service should use Registry.\n"+ - " -d, --dev Indicates service to run in developer mode which causes Host configuration values to be overridden.\n"+ - " with `localhost`. This is so that it will run with other services running in Docker (aka hybrid mode)\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"+ + " -cc, --commonConfig Takes the location where the common configuration is loaded from when\n"+ + " not using the Configuration Provider\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"+ + " -cf, --configFile Indicates name of the local configuration file. Defaults to configuration.yaml\n"+ + " -p, --profile Indicate configuration profile other than default\n"+ + " -cd, --configDir Specify local configuration directory\n"+ + " -r, --registry Indicates service should use Registry.\n"+ + " -rsh, \n"+ + " --remoteServiceHosts \n"+ + " Indicates that the service is running remote from the core EdgeX services and\n"+ + " to use the listed host names to connect remotely. contains 3 comma seperated host names seperated by ','.\n"+ + " 1st is the local system host name, 2nd is the remote system host name and 3rd is the WebServer bind host name\n"+ + " example: -rsh=192.0.1.20,192.0.1.5,localhost\n"+ + " -d, --dev Indicates service to run in developer mode which causes Host configuration values to be overridden.\n"+ + " with `localhost`. This is so that it will run with other services running in Docker (aka hybrid mode)\n"+ "%s\n"+ "Common Options:\n"+ - " -h, --help Show this message\n", + " -h, --help Show this message\n", os.Args[0], d.additionalUsage, ) os.Exit(0) diff --git a/bootstrap/flags/flags_test.go b/bootstrap/flags/flags_test.go index 8f46d1c8..0bd61d25 100644 --- a/bootstrap/flags/flags_test.go +++ b/bootstrap/flags/flags_test.go @@ -116,3 +116,10 @@ func TestConfigCommonScenario(t *testing.T) { assert.True(t, actual.UseRegistry()) assert.Equal(t, expectedConfigDirectory, actual.ConfigDirectory()) } + +func TestDefault_RemoteServiceHosts(t *testing.T) { + expected := []string{"1.2.3.4", "5.6.7.8", "localhost"} + actual := newSUT([]string{"--remoteServiceHosts=1.2.3.4,5.6.7.8,localhost"}) + + assert.Equal(t, expected, actual.RemoteServiceHosts()) +} diff --git a/bootstrap/handlers/clients.go b/bootstrap/handlers/clients.go index e3276182..7c3ef51b 100644 --- a/bootstrap/handlers/clients.go +++ b/bootstrap/handlers/clients.go @@ -37,15 +37,12 @@ import ( // ClientsBootstrap contains data to boostrap the configured clients type ClientsBootstrap struct { - registry registry.Client - inDevMode bool + registry registry.Client } // NewClientsBootstrap is a factory method that returns the initialized "ClientsBootstrap" receiver struct. -func NewClientsBootstrap(devMode bool) *ClientsBootstrap { - return &ClientsBootstrap{ - inDevMode: devMode, - } +func NewClientsBootstrap() *ClientsBootstrap { + return &ClientsBootstrap{} } // BootstrapHandler fulfills the BootstrapHandler contract. @@ -69,7 +66,7 @@ func (cb *ClientsBootstrap) BootstrapHandler( var err error if !serviceInfo.UseMessageBus { - url, err = cb.getClientUrl(serviceKey, serviceInfo.Url(), startupTimer, lc) + url, err = cb.getClientUrl(serviceKey, serviceInfo.Url(), startupTimer, dic, lc) if err != nil { lc.Error(err.Error()) return false @@ -164,8 +161,9 @@ func (cb *ClientsBootstrap) BootstrapHandler( return true } -func (cb *ClientsBootstrap) getClientUrl(serviceKey string, defaultUrl string, startupTimer startup.Timer, lc logger.LoggingClient) (string, error) { - if cb.registry == nil || cb.inDevMode { +func (cb *ClientsBootstrap) getClientUrl(serviceKey string, defaultUrl string, startupTimer startup.Timer, dic *di.Container, lc logger.LoggingClient) (string, error) { + mode := container.DevRemoteModeFrom(dic.Get) + if cb.registry == nil || mode.InDevMode || mode.InRemoteMode { lc.Infof("Using REST for '%s' clients @ %s", serviceKey, defaultUrl) return defaultUrl, nil } diff --git a/bootstrap/handlers/clients_test.go b/bootstrap/handlers/clients_test.go index 626fbf86..99e678c4 100644 --- a/bootstrap/handlers/clients_test.go +++ b/bootstrap/handlers/clients_test.go @@ -224,7 +224,7 @@ func TestClientsBootstrapHandler(t *testing.T) { }, }) - actualResult := NewClientsBootstrap(false).BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) + actualResult := NewClientsBootstrap().BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) require.Equal(t, actualResult, test.ExpectedResult) if test.ExpectedResult == false { return @@ -341,7 +341,7 @@ func TestCommandMessagingClientErrors(t *testing.T) { }) startupTimer := startup.NewTimer(1, 1) - actualResult := NewClientsBootstrap(false).BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) + actualResult := NewClientsBootstrap().BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) require.False(t, actualResult) mockLogger.AssertNumberOfCalls(t, "Errorf", 1)