diff --git a/Attribution.txt b/Attribution.txt deleted file mode 100644 index 90d44384f..000000000 --- a/Attribution.txt +++ /dev/null @@ -1,139 +0,0 @@ -The following open source projects are referenced by app-functions-sdk: - -bertimus9/systemstat (MIT) https://bitbucket.org/bertimus9/systemstat -https://bitbucket.org/bertimus9/systemstat/src/master/LICENSE - -armon/go-metrics (MIT) https://github.com/armon/go-metrics -https://github.com/armon/go-metrics/blob/master/LICENSE - -cenkalti/backoff (MIT) https://github.com/cenkalti/backoff -https://github.com/cenkalti/backoff/blob/master/LICENSE - -davecgh/go-spew (ISC) https://github.com/davecgh/go-spew -https://github.com/davecgh/go-spew/blob/master/LICENSE - -pmezard/go-difflib (Unspecified) https://github.com/pmezard/go-difflib -https://github.com/pmezard/go-difflib/blob/master/LICENSE - -stretchr/objx (MIT) https://github.com/stretchr/objx -https://github.com/stretchr/objx/blob/master/LICENSE - -stretchr/testify (MIT) https://github.com/stretchr/testify -https://github.com/stretchr/testify/blob/master/LICENSE - -eclipse/paho.mqtt.golang (Eclipse Public License 1.0) https://github.com/eclipse/paho.mqtt.golang -https://github.com/eclipse/paho.mqtt.golang/blob/master/LICENSE - -edgexfoundry/app-functions-sdk-go (Apache 2.0) https://github.com/edgexfoundry/app-functions-sdk-go -https://github.com/edgexfoundry/app-functions-sdk-go/blob/master/LICENSE - -edgexfoundry/go-mod-core-contracts (Apache 2.0) https://github.com/edgexfoundry/go-mod-core-contracts -https://github.com/edgexfoundry/go-mod-core-contracts/blob/master/LICENSE - -edgexfoundry/go-mod-registry (Apache 2.0) https://github.com/edgexfoundry/go-mod-registry -https://github.com/edgexfoundry/go-mod-registry/blob/master/LICENSE - -edgexfoundry/go-mod-messaging (Apache 2.0) https://github.com/edgexfoundry/go-mod-messaging -https://github.com/edgexfoundry/go-mod-messaging/blob/master/LICENSE - -edgexfoundry/go-mod-secrets (Apache 2.0) https://github.com/edgexfoundry/go-mod-secrets -https://github.com/edgexfoundry/go-mod-secrets/blob/master/LICENSE - -BurntSushi/toml (MIT) https://github.com/BurntSushi/toml -https://github.com/BurntSushi/toml/blob/master/COPYING - -go-kit/kit (MIT) github.com/go-kit/kit -https://github.com/go-kit/kit/blob/master/LICENSE - -go-logfmt/logfmt (MIT) https://github.com/go-logfmt/logfmt -https://github.com/go-logfmt/logfmt/blob/master/LICENSE - -google/uuid (BSD-3) https://github.com/google/uuid -https://github.com/google/uuid/blob/master/LICENSE - -gorilla/mux (BSD-3) https://github.com/gorilla/mux -https://github.com/gorilla/mux/blob/master/LICENSE - -hashicorp/consul/api (Mozilla Public License 2.0) - https://github.com/hashicorp/consul/api -https://github.com/hashicorp/consul/blob/master/LICENSE - -hashicorp/go-cleanhttp (Mozilla Public License 2.0) - https://github.com/hashicorp/go-cleanhttp -https://github.com/hashicorp/go-cleanhttp/blob/master/LICENSE - -hashicorp/go-immutable-radix (Mozilla Public License 2.0) https://github.com/hashicorp/go-immutable-radix -https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE - -hashicorp/go-rootcerts (Mozilla Public License 2.0) https://github.com/hashicorp/go-rootcerts -https://github.com/hashicorp/go-rootcerts/blob/master/LICENSE - -hashicorp/golang-lru (Mozilla Public License 2.0) https://github.com/hashicorp/golang-lru -https://github.com/hashicorp/golang-lru/blob/master/LICENSE - -hashicorp/serf (Mozilla Public License 2.0) https://github.com/hashicorp/serf -https://github.com/hashicorp/serf/blob/master/LICENSE - -kr/logfmt (Unspecified) https://github.com/kr/logfmt -https://github.com/kr/logfmt/blob/master/Readme - -mitchellh/consulstructure (MIT) https://github.com/mitchellh/consulstructure -https://github.com/mitchellh/consulstructure/blob/master/LICENSE - -mitchellh/mapstructure (MIT) https://github.com/mitchellh/mapstructure -https://github.com/mitchellh/mapstructure/blob/master/LICENSE - -mitchellh/copystructure (MIT) https://github.com/mitchellh/copystructure -https://github.com/mitchellh/copystructure/blob/master/LICENSE - -mitchellh/reflectwalk (MIT) https://github.com/mitchellh/reflectwalk -https://github.com/mitchellh/reflectwalk/blob/master/LICENSE - -mitchellh/go-homedir (MIT) https://github.com/mitchellh/go-homedir -https://github.com/mitchellh/go-homedir/blob/master/LICENSE - -pebbe/zmq4 (BSD-2) https://github.com/pebbe/zmq4 -https://github.com/pebbe/zmq4/blob/master/LICENSE.txt - -pelletier/go-toml (MIT) https://github.com/pelletier/go-toml -https://github.com/pelletier/go-toml/blob/master/LICENSE - -pkg/errors (BSD-2) https://github.com/pkg/errors -https://github.com/pkg/errors/blob/master/LICENSE - -ugorji/go (MIT) https://github.com/ugorji/go -https://github.com/ugorji/go/blob/master/LICENSE - -golang.org/x/net (Unspecified) https://github.com/golang/net -https://github.com/golang/net/blob/master/LICENSE - -gomodule/redigo (Apache 2.0) https://github.com/gomodule/redigo -https://github.com/gomodule/redigo/blob/master/LICENSE - -github.com/go-stack/stack (MIT) https://github.com/go-stack/stack -https://github.com/go-stack/stack/blob/master/LICENSE.md - -github.com/golang/snappy (BSD-3) https://github.com/golang/snappy -https://github.com/golang/snappy/blob/master/LICENSE - -github.com/xdg/scram (Apache 2.0) https://github.com/xdg-go/scram -https://github.com/xdg-go/scram/blob/master/LICENSE - -github.com/xdg/stringprep (Apache 2.0) https://github.com/xdg-go/stringprep -https://github.com/xdg-go/stringprep/blob/master/LICENSE - -go.mongodb.org/mongo-driver (Apache 2.0) https://github.com/mongodb/mongo-go-driver -https://github.com/mongodb/mongo-go-driver/blob/master/LICENSE - -golang.org/x/crypto (Unspecified) https://github.com/golang/crypto -https://github.com/golang/crypto/blob/master/LICENSE - -golang.org/x/sync (Unspecified) https://github.com/golang/sync -https://github.com/golang/sync/blob/master/LICENSE - -golang.org/x/text (Unspecified) https://github.com/golang/text -https://github.com/golang/text/blob/master/LICENSE - -gopkg.in/yaml.v2 (Apache 2.0) https://github.com/go-yaml/yaml -https://github.com/go-yaml/yaml/blob/v2/LICENSE - -github.com/diegoholiveira/jsonlogic (MIT) https://github.com/diegoholiveira/ -https://github.com/diegoholiveira/jsonlogic/blob/master/LICENSE \ No newline at end of file diff --git a/Makefile b/Makefile index 2e5655eba..b1ac36e1e 100644 --- a/Makefile +++ b/Makefile @@ -7,5 +7,4 @@ test: $(GO) vet ./... gofmt -l . [ "`gofmt -l .`" = "" ] - ./bin/test-attribution-txt.sh ./bin/test-go-mod-tidy.sh \ No newline at end of file diff --git a/appcontext/context.go b/appcontext/context.go index 8bd2b89cb..dc36b1377 100644 --- a/appcontext/context.go +++ b/appcontext/context.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ type Context struct { // OutputData is used for specifying the data that is to be outputted. Leverage the .Complete() function to set. OutputData []byte // This holds the configuration for your service. This is the preferred way to access your custom application settings that have been set in the configuration. - Configuration common.ConfigurationStruct + Configuration *common.ConfigurationStruct // LoggingClient is exposed to allow logging following the preferred logging strategy within EdgeX. LoggingClient logger.LoggingClient // EventClient exposes Core Data's EventClient API @@ -99,6 +99,10 @@ func (context *Context) SetRetryData(payload []byte) { // CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. func (context *Context) PushToCoreData(deviceName string, readingName string, value interface{}) (*models.Event, error) { context.LoggingClient.Debug("Pushing to CoreData") + if context.EventClient == nil { + return nil, fmt.Errorf("unable to Push To CoreData: '%s' is missing from Clients configuration", common.CoreDataClientName) + } + now := time.Now().UnixNano() val, err := util.CoerceType(value) if err != nil { diff --git a/appsdk/configupdates.go b/appsdk/configupdates.go new file mode 100644 index 000000000..452614f27 --- /dev/null +++ b/appsdk/configupdates.go @@ -0,0 +1,185 @@ +// +// Copyright (c) 2020 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 appsdk + +import ( + "context" + "fmt" + "reflect" + "sync" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/config" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/handlers" +) + +// ConfigUpdateProcessor contains the data need to process configuration updates +type ConfigUpdateProcessor struct { + sdk *AppFunctionsSDK +} + +// NewConfigUpdateProcessor creates a new ConfigUpdateProcessor which process configuration updates triggered from +// the Configuration Provider +func NewConfigUpdateProcessor(sdk *AppFunctionsSDK) *ConfigUpdateProcessor { + return &ConfigUpdateProcessor{sdk: sdk} +} + +// WaitForConfigUpdates waits for signal that configuration has been updated (triggered from by Configuration Provider) +// and then determines what was updated and does any special processing, if needed, for the updates. +func (processor *ConfigUpdateProcessor) WaitForConfigUpdates(configUpdated config.UpdatedStream) { + sdk := processor.sdk + sdk.appWg.Add(1) + + go func() { + defer sdk.appWg.Done() + + sdk.LoggingClient.Info("Waiting for App Service configuration updates...") + + previousWriteable := sdk.config.Writable + + for { + select { + case <-sdk.appCtx.Done(): + sdk.LoggingClient.Info("Exiting waiting for App Service configuration updates") + return + + case <-configUpdated: + currentWritable := sdk.config.Writable + // Verify something actually changed, if not no need to process anything. + // This can happen on initial startup + if reflect.DeepEqual(currentWritable, previousWriteable) { + sdk.LoggingClient.Info("No actual configuration changes detected. Skipping processing updates.") + continue + } + + sdk.LoggingClient.Info("Processing App Service configuration updates") + + // Note: Updates occur one setting at a time so only have to look for single changes + switch { + case previousWriteable.LogLevel != currentWritable.LogLevel: + _ = sdk.LoggingClient.SetLogLevel(currentWritable.LogLevel) + sdk.LoggingClient.Info(fmt.Sprintf("Logging level changed to %s", currentWritable.LogLevel)) + + case !reflect.DeepEqual(previousWriteable.InsecureSecrets, currentWritable.InsecureSecrets): + sdk.LoggingClient.Info("Insecure Secrets have been updated") + + case previousWriteable.StoreAndForward.MaxRetryCount != currentWritable.StoreAndForward.MaxRetryCount: + if currentWritable.StoreAndForward.MaxRetryCount < 0 { + sdk.LoggingClient.Warn( + fmt.Sprintf("StoreAndForward MaxRetryCount can not be less than 0, defaulting to 1")) + currentWritable.StoreAndForward.MaxRetryCount = 1 + } + sdk.LoggingClient.Info( + fmt.Sprintf( + "StoreAndForward MaxRetryCount changed to %d", + currentWritable.StoreAndForward.MaxRetryCount)) + + case previousWriteable.StoreAndForward.RetryInterval != currentWritable.StoreAndForward.RetryInterval: + processor.processConfigChangedStoreForwardRetryInterval() + + case previousWriteable.StoreAndForward.Enabled != currentWritable.StoreAndForward.Enabled: + processor.processConfigChangedStoreForwardEnabled() + + case !reflect.DeepEqual(previousWriteable.Pipeline, currentWritable.Pipeline): + processor.processConfigChangedPipeline() + + default: + // Nothing of interest changed + sdk.LoggingClient.Info("No configuration changes detected that require special processing.") + } + + // grab new copy of the writeable configuration for comparing against when next update occurs + previousWriteable = currentWritable + } + } + }() +} + +// processConfigChangedStoreForwardRetryInterval handles when the Store and Forward RetryInterval setting has been updated +func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardRetryInterval() { + sdk := processor.sdk + + if sdk.config.Writable.StoreAndForward.Enabled { + sdk.stopStoreForward() + sdk.startStoreForward() + } +} + +// processConfigChangedStoreForwardEnabled handles when the Store and Forward Enabled setting has been updated +func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardEnabled() { + sdk := processor.sdk + + if sdk.config.Writable.StoreAndForward.Enabled { + // StoreClient must be set up for StoreAndForward + if sdk.storeClient == nil { + var err error + sdk.storeClient, err = handlers.InitializeStoreClient(sdk.secretProvider, sdk.config) + if err != nil { + // Error already logged + sdk.config.Writable.StoreAndForward.Enabled = false + return + } + + sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) + } + + sdk.startStoreForward() + } else { + sdk.stopStoreForward() + } +} + +// processConfigChangedPipeline handles when any of the Pipeline settings have been updated +func (processor *ConfigUpdateProcessor) processConfigChangedPipeline() { + sdk := processor.sdk + + if sdk.usingConfigurablePipeline { + transforms, err := sdk.LoadConfigurablePipeline() + if err != nil { + sdk.LoggingClient.Error("unable to reload Configurable Pipeline from Registry: " + err.Error()) + return + } + err = sdk.SetFunctionsPipeline(transforms...) + if err != nil { + sdk.LoggingClient.Error("unable to set Configurable Pipeline from Registry: " + err.Error()) + return + } + + sdk.LoggingClient.Info("Reloaded Configurable Pipeline from Registry") + } +} + +// startStoreForward starts the Store and Forward processing +func (sdk *AppFunctionsSDK) startStoreForward() { + var storeForwardEnabledCtx context.Context + sdk.storeForwardWg = &sync.WaitGroup{} + storeForwardEnabledCtx, sdk.storeForwardCancelCtx = context.WithCancel(context.Background()) + sdk.runtime.StartStoreAndForward( + sdk.appWg, + sdk.appCtx, + sdk.storeForwardWg, + storeForwardEnabledCtx, + sdk.ServiceKey, + sdk.config, + sdk.edgexClients) +} + +// stopStoreForward stops the Store and Forward processing +func (sdk *AppFunctionsSDK) stopStoreForward() { + sdk.LoggingClient.Info("Canceling Store and Forward retry loop") + sdk.storeForwardCancelCtx() + sdk.storeForwardWg.Wait() +} diff --git a/appsdk/sdk.go b/appsdk/sdk.go index b021e0755..0bf644606 100644 --- a/appsdk/sdk.go +++ b/appsdk/sdk.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,9 +18,7 @@ package appsdk import ( "context" - "encoding/json" "errors" - "flag" "fmt" nethttp "net/http" "os" @@ -29,43 +27,39 @@ import ( "strings" "sync" "syscall" - "time" - - "github.com/gorilla/mux" - toml "github.com/pelletier/go-toml" "github.com/edgexfoundry/go-mod-core-contracts/clients" - "github.com/edgexfoundry/go-mod-core-contracts/clients/command" - "github.com/edgexfoundry/go-mod-core-contracts/clients/coredata" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/clients/notifications" "github.com/edgexfoundry/go-mod-core-contracts/models" - "github.com/edgexfoundry/go-mod-registry/pkg/types" "github.com/edgexfoundry/go-mod-registry/registry" + "github.com/gorilla/mux" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/config" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/flags" + bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" "github.com/edgexfoundry/app-functions-sdk-go/appcontext" "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/handlers" "github.com/edgexfoundry/app-functions-sdk-go/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/internal/config" "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" "github.com/edgexfoundry/app-functions-sdk-go/internal/security" - "github.com/edgexfoundry/app-functions-sdk-go/internal/store" "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db/interfaces" - "github.com/edgexfoundry/app-functions-sdk-go/internal/telemetry" "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger" "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/internal/webserver" - "github.com/edgexfoundry/app-functions-sdk-go/pkg/urlclient" "github.com/edgexfoundry/app-functions-sdk-go/pkg/util" ) const ( // ProfileSuffixPlaceholder is used to create unique names for profiles - ProfileSuffixPlaceholder = "" - ProfileEnvironmentVariable = "edgex_profile" - CoreServiceVersionKey = "version" - MajorIndex = 0 + ProfileSuffixPlaceholder = "" ) // The key type is unexported to prevent collisions with context keys defined in @@ -91,18 +85,14 @@ type AppFunctionsSDK struct { // except when &[]byte{} is specified. In this case the []byte data is pass to the first function in the Pipeline. TargetType interface{} transforms []appcontext.AppFunction - configProfile string - configDir string - useRegistry bool skipVersionCheck bool - overwriteConfig bool usingConfigurablePipeline bool httpErrors chan error runtime *runtime.GolangRuntime webserver *webserver.WebServer edgexClients common.EdgeXClients registryClient registry.Client - config common.ConfigurationStruct + config *common.ConfigurationStruct storeClient interfaces.StoreClient secretProvider *security.SecretProvider storeForwardWg *sync.WaitGroup @@ -110,6 +100,7 @@ type AppFunctionsSDK struct { appWg *sync.WaitGroup appCtx context.Context appCancelCtx context.CancelFunc + deferredFunctions []bootstrap.Deferred } // AddRoute allows you to leverage the existing webserver to add routes. @@ -142,11 +133,14 @@ func (sdk *AppFunctionsSDK) MakeItRun() error { t := sdk.setupTrigger(sdk.config, sdk.runtime) // Initialize the trigger (i.e. start a web server, or connect to message bus) - err := t.Initialize(sdk.appWg, sdk.appCtx) + deferred, err := t.Initialize(sdk.appWg, sdk.appCtx) if err != nil { sdk.LoggingClient.Error(err.Error()) } + // deferred is a a function that needs to be called when services exits. + sdk.addDeferred(deferred) + if sdk.config.Writable.StoreAndForward.Enabled { sdk.startStoreForward() } else { @@ -176,6 +170,13 @@ func (sdk *AppFunctionsSDK) MakeItRun() error { sdk.appCancelCtx() // Cancel all long running go funcs sdk.appWg.Wait() + + // Call all the deferred funcs that need to happen when exiting. + // These are things like un-register from the Registry, disconnect from the Message Bus, etc + for _, deferredFunc := range sdk.deferredFunctions { + deferredFunc() + } + return err } @@ -294,24 +295,15 @@ func (sdk *AppFunctionsSDK) GetAppSettingStrings(setting string) ([]string, erro // Initialize will parse command line flags, register for interrupts, // initialize the logging system, and ingest configuration. func (sdk *AppFunctionsSDK) Initialize() error { - applyCommandlineEnvironmentOverrides() - - flag.BoolVar(&sdk.useRegistry, "registry", false, "Indicates the service should use the registry.") - flag.BoolVar(&sdk.useRegistry, "r", false, "Indicates the service should use registry.") - - flag.StringVar(&sdk.configProfile, "profile", "", "Specify a profile other than default.") - flag.StringVar(&sdk.configProfile, "p", "", "Specify a profile other than default.") - - flag.StringVar(&sdk.configDir, "confdir", "", "Specify an alternate configuration directory.") - flag.StringVar(&sdk.configDir, "c", "", "Specify an alternate configuration directory.") + startupTimer := startup.NewStartUpTimer(internal.BootRetrySecondsDefault, internal.BootTimeoutSecondsDefault) - flag.BoolVar(&sdk.skipVersionCheck, "skipVersionCheck", false, "Indicates the service should skip the Core Service's version compatibility check.") - flag.BoolVar(&sdk.skipVersionCheck, "s", false, "Indicates the service should skip the Core Service's version compatibility check.") + additionalUsage := + " -s/--skipVersionCheck Indicates the service should skip the Core Service's version compatibility check." - flag.BoolVar(&sdk.overwriteConfig, "overwrite", false, "Overwrite configuration in the Registry with local values") - flag.BoolVar(&sdk.overwriteConfig, "o", false, "Overwrite configuration in the Registry with local values") - - flag.Parse() + sdkFlags := flags.NewWithUsage(additionalUsage) + sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "skipVersionCheck", false, "") + sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "s", false, "") + sdkFlags.Parse(os.Args[1:]) // Service keys must be unique. If an executable is run multiple times, it must have a different // profile for each instance, thus adding the profile to the base key will make it unique. @@ -320,223 +312,94 @@ func (sdk *AppFunctionsSDK) Initialize() error { // // The Dockerfile must also take this into account and set the profile appropriately, i.e. not just "docker" // - if strings.Contains(sdk.ServiceKey, ProfileSuffixPlaceholder) { - if sdk.configProfile == "" { + if sdkFlags.Profile() == "" { sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, "", 1) } else { - sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, "-"+sdk.configProfile, 1) - } - } - - // to first initialize the app context and cancel function as the context - // is being used inside sdk.initializeSecretProvider() call below - sdk.appCtx, sdk.appCancelCtx = context.WithCancel(context.Background()) - - loggerInitialized := false - databaseInitialized := false - configurationInitialized := false - bootstrapComplete := false - secretProviderInitialized := false - - timeStart := time.Now() - // Currently have to load configuration from filesystem first in order to obtain - // Registry Host/Port and BootTimeout - configuration, err := readConfigurationFromFile(sdk.configProfile, sdk.configDir) - if err != nil { - return err - } - - bootTimeout, err := time.ParseDuration(configuration.Service.BootTimeout) - if err != nil { - fmt.Printf("warning- failed to parse Service.BootTimeout, use the default %s: %v\n", - internal.BootTimeoutDefault.String(), err) - bootTimeout = internal.BootTimeoutDefault - } - - timeElapsed := time.Since(timeStart) - - // Bootstrap retry loop to ensure all dependencies are ready before continuing. - until := time.Now().Add(bootTimeout - timeElapsed) - for time.Now().Before(until) { - if !configurationInitialized { - err := sdk.initializeConfiguration(configuration) - if err != nil { - fmt.Printf("failed to initialize Registry: %v\n", err) - goto ContinueWithSleep - } - configurationInitialized = true - fmt.Printf("Configuration & Registry initialized") - } - - if !loggerInitialized { - loggingTarget, err := sdk.setLoggingTarget() - if err != nil { - fmt.Printf("logger initialization failed: %v", err) - goto ContinueWithSleep - } - - sdk.LoggingClient = logger.NewClient( - sdk.ServiceKey, - sdk.config.Logging.EnableRemote, - loggingTarget, - sdk.config.Writable.LogLevel, - ) - sdk.LoggingClient.Info("Logger successfully initialized") - sdk.edgexClients.LoggingClient = sdk.LoggingClient - loggerInitialized = true - } - - // Verify that Core Services major version matches this SDK's major version - if !sdk.validateVersionMatch() { - return fmt.Errorf("core service's version is not compatible with SDK's version") - } - - if !secretProviderInitialized { - if err := sdk.initializeSecretProvider(); err != nil { - return err - } - secretProviderInitialized = true + sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, "-"+sdkFlags.Profile(), 1) } - - // Currently only need the database if store and forward is enabled - if sdk.config.Writable.StoreAndForward.Enabled { - if !databaseInitialized { - if sdk.initializeStoreClient() != nil { - - // Error already logged - goto ContinueWithSleep - } - - databaseInitialized = true - } - } - - sdk.initializeClients() - sdk.LoggingClient.Info("Clients initialized") - - // This is the last dependency so can break out of the retry loop. - bootstrapComplete = true - break - - ContinueWithSleep: - time.Sleep(time.Second * time.Duration(1)) } - if !bootstrapComplete { - return fmt.Errorf("bootstrap retry timed out") - } + sdk.config = &common.ConfigurationStruct{} + dic := di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return sdk.config + }, + }) + sdk.appCtx, sdk.appCancelCtx = context.WithCancel(context.Background()) sdk.appWg = &sync.WaitGroup{} - if sdk.useRegistry { - sdk.appWg.Add(1) - go sdk.listenForConfigChanges() - } + var deferred bootstrap.Deferred + var successful bool + var configUpdated config.UpdatedStream = make(chan struct{}) + + sdk.appWg, deferred, successful = bootstrap.RunAndReturnWaitGroup( + sdk.appCtx, + sdk.appCancelCtx, + sdkFlags, + sdk.ServiceKey, + internal.ConfigRegistryStem, + sdk.config, + configUpdated, + startupTimer, + dic, + []bootstrapInterfaces.BootstrapHandler{ + handlers.NewSecrets().BootstrapHandler, + handlers.NewDatabase().BootstrapHandler, + handlers.NewClients().BootstrapHandler, + handlers.NewTelemetry().BootstrapHandler, + handlers.NewVersionValidator(sdk.skipVersionCheck, internal.SDKVersion).BootstrapHandler, + }, + ) + + // deferred is a a function that needs to be called when services exits. + sdk.addDeferred(deferred) + + if !successful { + return fmt.Errorf("boostrapping failed") + } + + // Bootstrapping is complete, so now need to retrieve the needed objects from the containers. + sdk.secretProvider = container.SecretProviderFrom(dic.Get) + sdk.storeClient = container.StoreClientFrom(dic.Get) + sdk.LoggingClient = bootstrapContainer.LoggingClientFrom(dic.Get) + sdk.edgexClients.LoggingClient = sdk.LoggingClient + sdk.edgexClients.EventClient = container.EventClientFrom(dic.Get) + sdk.edgexClients.ValueDescriptorClient = container.ValueDescriptorClientFrom(dic.Get) + sdk.edgexClients.NotificationsClient = container.NotificationsClientFrom(dic.Get) + sdk.edgexClients.CommandClient = container.CommandClientFrom(dic.Get) - sdk.appWg.Add(1) - go telemetry.StartCpuUsageAverage(sdk.appWg, sdk.appCtx, sdk.LoggingClient) + // We do special processing when the writeable section of the configuration changes, so have + // to wait to be signaled when the configuration has been updated and then process the changes + NewConfigUpdateProcessor(sdk).WaitForConfigUpdates(configUpdated) - sdk.webserver = webserver.NewWebServer(&sdk.config, sdk.secretProvider, sdk.LoggingClient, mux.NewRouter()) + sdk.webserver = webserver.NewWebServer(sdk.config, sdk.secretProvider, sdk.LoggingClient, mux.NewRouter()) sdk.webserver.ConfigureStandardRoutes() return nil } -func (sdk *AppFunctionsSDK) initializeSecretProvider() error { - - sdk.secretProvider = security.NewSecretProvider(sdk.LoggingClient, &sdk.config) - ok := sdk.secretProvider.Initialize(sdk.appCtx) - if !ok { - err := errors.New("unable to initialize secret provider") - sdk.LoggingClient.Error(err.Error()) - return err - } - - return nil -} - -func (sdk *AppFunctionsSDK) initializeStoreClient() error { - var err error - - credentials, err := sdk.secretProvider.GetDatabaseCredentials(sdk.config.Database) - if err != nil { - sdk.LoggingClient.Error("Unable to get Database Credentials", "error", err) - } - - sdk.config.Database.Username = credentials.Username - sdk.config.Database.Password = credentials.Password - - sdk.storeClient, err = store.NewStoreClient(sdk.config.Database) - if err != nil { - sdk.LoggingClient.Error(fmt.Sprintf("unable to initialize Database for Store and Forward: %s", err.Error())) - } - - return err +// GetSecrets retrieves secrets from a secret store. +// path specifies the type or location of the secrets to retrieve. If specified it is appended +// to the base path from the SecretConfig +// keys specifies the secrets which to retrieve. If no keys are provided then all the keys associated with the +// specified path will be returned. +func (sdk *AppFunctionsSDK) GetSecrets(path string, keys ...string) (map[string]string, error) { + return sdk.secretProvider.GetSecrets(path, keys...) } -func (sdk *AppFunctionsSDK) validateVersionMatch() bool { - if sdk.skipVersionCheck { - sdk.LoggingClient.Info("Skipping core service version compatibility check") - return true - } - - // SDK version is set via the SemVer TAG at build time - // and has the format "v{major}.{minor}.{patch}[-dev.{build}]" - sdkVersionParts := strings.Split(internal.SDKVersion, ".") - if len(sdkVersionParts) < 3 { - sdk.LoggingClient.Error("SDK version is malformed", "version", internal.SDKVersion) - return false - } - - sdkVersionParts[MajorIndex] = strings.Replace(sdkVersionParts[MajorIndex], "v", "", 1) - if sdkVersionParts[MajorIndex] == "0" { - sdk.LoggingClient.Info("Skipping core service version compatibility check for SDK Beta version or running in debugger", "version", internal.SDKVersion) - return true - } - - url := sdk.config.Clients[common.CoreDataClientName].Url() + clients.ApiVersionRoute - data, err := clients.GetRequestWithURL(context.Background(), url) - if err != nil { - sdk.LoggingClient.Error("Unable to get version of Core Services", "error", err) - return false - } - - versionJson := map[string]string{} - err = json.Unmarshal(data, &versionJson) - if err != nil { - sdk.LoggingClient.Error("Unable to un-marshal Core Services version data", "error", err) - return false - } - - version, ok := versionJson[CoreServiceVersionKey] - if !ok { - sdk.LoggingClient.Error(fmt.Sprintf("Core Services version data missing '%s' information", CoreServiceVersionKey)) - return false - } - - // Core Service version is reported as "{major}.{minor}.{patch}" - coreVersionParts := strings.Split(version, ".") - if len(coreVersionParts) != 3 { - sdk.LoggingClient.Error("Core Services version is malformed", "version", version) - return false - } - - // Do Major versions match? - if coreVersionParts[0] == sdkVersionParts[0] { - sdk.LoggingClient.Debug( - fmt.Sprintf("Confirmed Core Services version (%s) is compatible with SDK's version (%s)", - version, internal.SDKVersion)) - return true - } - - sdk.LoggingClient.Error(fmt.Sprintf("Core services version (%s) is not compatible with SDK's version(%s)", - version, internal.SDKVersion)) - return false +// StoreSecrets stores the secrets to a secret store. +// it sets the values requested at provided keys +// path specifies the type or location of the secrets to store. If specified it is appended +// to the base path from the SecretConfig +// secrets map specifies the "key": "value" pairs of secrets to store +func (sdk *AppFunctionsSDK) StoreSecrets(path string, secrets map[string]string) error { + return sdk.secretProvider.StoreSecrets(path, secrets) } // setupTrigger configures the appropriate trigger as specified by configuration. -func (sdk *AppFunctionsSDK) setupTrigger(configuration common.ConfigurationStruct, runtime *runtime.GolangRuntime) trigger.Trigger { +func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) trigger.Trigger { var t trigger.Trigger // Need to make dynamic, search for the binding that is input @@ -559,354 +422,8 @@ func (sdk *AppFunctionsSDK) addContext(next func(nethttp.ResponseWriter, *nethtt } } -func (sdk *AppFunctionsSDK) initializeClients() { - // Need when passing all Clients to other components - sdk.edgexClients.LoggingClient = sdk.LoggingClient - wg := &sync.WaitGroup{} - clientMonitor, err := time.ParseDuration(sdk.config.Service.ClientMonitor) - if err != nil { - sdk.LoggingClient.Warn( - fmt.Sprintf( - "Service.ClientMonitor failed to parse: %s, use the default value: %v", - err, - internal.ClientMonitorDefault, - ), - ) - // fall back to default value - clientMonitor = internal.ClientMonitorDefault - } - - interval := int(clientMonitor / time.Millisecond) - - // Use of these client interfaces is optional, so they are not required to be configured. For instance if not - // sending commands, then don't need to have the Command client in the configuration. - if _, ok := sdk.config.Clients[common.CoreDataClientName]; ok { - sdk.edgexClients.EventClient = coredata.NewEventClient( - urlclient.New( - context.Background(), - wg, - sdk.registryClient, - clients.CoreDataServiceKey, - clients.ApiEventRoute, - interval, - sdk.config.Clients[common.CoreDataClientName].Url()+clients.ApiEventRoute, - ), - ) - - sdk.edgexClients.ValueDescriptorClient = coredata.NewValueDescriptorClient( - urlclient.New( - context.Background(), - wg, - sdk.registryClient, - clients.CoreDataServiceKey, - clients.ApiValueDescriptorRoute, - interval, - sdk.config.Clients[common.CoreDataClientName].Url()+clients.ApiValueDescriptorRoute, - ), - ) - } - - if _, ok := sdk.config.Clients[common.CoreCommandClientName]; ok { - sdk.edgexClients.CommandClient = command.NewCommandClient( - urlclient.New( - context.Background(), - wg, - sdk.registryClient, - clients.CoreCommandServiceKey, - clients.ApiDeviceRoute, - interval, - sdk.config.Clients[common.CoreCommandClientName].Url()+clients.ApiDeviceRoute, - ), - ) - } - - if _, ok := sdk.config.Clients[common.NotificationsClientName]; ok { - sdk.edgexClients.NotificationsClient = notifications.NewNotificationsClient( - urlclient.New( - context.Background(), - wg, - sdk.registryClient, - clients.SupportNotificationsServiceKey, - clients.ApiNotificationRoute, - interval, - sdk.config.Clients[common.NotificationsClientName].Url()+clients.ApiNotificationRoute, - ), - ) - } -} - -func (sdk *AppFunctionsSDK) initializeConfiguration(configuration *common.ConfigurationStruct) error { - if sdk.useRegistry { - e := config.NewEnvironment() - configuration.Registry = e.OverrideRegistryInfoFromEnvironment(configuration.Registry) - configuration.Service = e.OverrideServiceInfoFromEnvironment(configuration.Service) - - if _, err := time.ParseDuration(configuration.Service.CheckInterval); err != nil { - return fmt.Errorf("failed to parse Service.CheckInterval: %v", err) - } - - registryConfig := types.Config{ - Host: configuration.Registry.Host, - Port: configuration.Registry.Port, - Type: configuration.Registry.Type, - Stem: internal.ConfigRegistryStem, - CheckInterval: configuration.Service.CheckInterval, - CheckRoute: clients.ApiPingRoute, - ServiceKey: sdk.ServiceKey, - ServiceHost: configuration.Service.Host, - ServicePort: configuration.Service.Port, - ServiceProtocol: configuration.Service.Protocol, - } - - client, err := registry.NewRegistryClient(registryConfig) - if err != nil { - return fmt.Errorf("connection to Registry could not be made: %v", err) - } - - // set registryClient - sdk.registryClient = client - - if !sdk.registryClient.IsAlive() { - return fmt.Errorf("registry (%s) is not running", registryConfig.Type) - } - - hasConfig, err := sdk.registryClient.HasConfiguration() - if err != nil { - return fmt.Errorf("could not determine if registry has configuration: %v", err) - } - - if !sdk.overwriteConfig && hasConfig { - rawConfig, err := sdk.registryClient.GetConfiguration(configuration) - if err != nil { - return fmt.Errorf("could not get configuration from Registry: %v", err) - } - - actual, ok := rawConfig.(*common.ConfigurationStruct) - if !ok { - return fmt.Errorf("configuration from Registry failed type check") - } - configuration = actual - - // Check that information was successfully read from Consul - if configuration.Service.Port == 0 { - sdk.LoggingClient.Error("Error reading from registry") - } - - fmt.Println("Configuration loaded from registry with service key: " + sdk.ServiceKey) - } else { - // Marshal into a toml Tree for overriding with environment variables. - contents, err := toml.Marshal(*configuration) - if err != nil { - return err - } - configTree, err := toml.LoadBytes(contents) - if err != nil { - return err - } - - err = sdk.registryClient.PutConfigurationToml(e.OverrideFromEnvironment(configTree), true) - if err != nil { - return fmt.Errorf("could not push configuration into registry: %v", err) - } - err = configTree.Unmarshal(configuration) - if err != nil { - return fmt.Errorf("could not marshal configTree to configuration: %v", err.Error()) - } - fmt.Println("Configuration pushed to registry with service key: " + sdk.ServiceKey) - } - - // Register the service with Registry - err = sdk.registryClient.Register() - if err != nil { - return fmt.Errorf("could not register service with Registry: %v", err) - } - } - - sdk.config = *configuration - return nil -} - -func (sdk *AppFunctionsSDK) listenForConfigChanges() { - - updates := make(chan interface{}) - registryErrors := make(chan error) - - defer sdk.appWg.Done() - defer close(updates) - - sdk.LoggingClient.Info("Listening for changes from registry") - sdk.registryClient.WatchForChanges(updates, registryErrors, &common.WritableInfo{}, internal.WritableKey) - - for { - select { - case <-sdk.appCtx.Done(): - sdk.LoggingClient.Info("Exiting Listen for changes from registry") - return - - case err := <-registryErrors: - sdk.LoggingClient.Error(err.Error()) - - case raw, ok := <-updates: - if !ok { - sdk.LoggingClient.Error("Failed to receive changes from update channel") - return - } - - actual, ok := raw.(*common.WritableInfo) - if !ok { - sdk.LoggingClient.Error("listenForConfigChanges() type check failed") - return - } - - previousLogLevel := sdk.config.Writable.LogLevel - previousStoreForward := sdk.config.Writable.StoreAndForward - - sdk.config.Writable = *actual - sdk.LoggingClient.Info("Writable configuration has been updated from Registry") - - // Note: Changes occur one setting at a time so if setting not part of the pipeline, - // then skip updating the pipeline - switch { - case previousLogLevel != sdk.config.Writable.LogLevel: - _ = sdk.LoggingClient.SetLogLevel(sdk.config.Writable.LogLevel) - sdk.LoggingClient.Info(fmt.Sprintf("Logging level changed to %s", sdk.config.Writable.LogLevel)) - - case previousStoreForward.MaxRetryCount != sdk.config.Writable.StoreAndForward.MaxRetryCount: - if sdk.config.Writable.StoreAndForward.MaxRetryCount < 0 { - sdk.LoggingClient.Warn(fmt.Sprintf("StoreAndForward MaxRetryCount can not be less than 0, defaulting to 1")) - sdk.config.Writable.StoreAndForward.MaxRetryCount = 1 - } - sdk.LoggingClient.Info(fmt.Sprintf("StoreAndForward MaxRetryCount changed to %d", sdk.config.Writable.StoreAndForward.MaxRetryCount)) - - case previousStoreForward.RetryInterval != sdk.config.Writable.StoreAndForward.RetryInterval: - sdk.processConfigChangedStoreForwardRetryInterval() - - case previousStoreForward.Enabled != sdk.config.Writable.StoreAndForward.Enabled: - sdk.processConfigChangedStoreForwardEnabled() - - default: - // Must have been a change to the pipeline configuration, so now attempt to update it. - sdk.processConfigChangedPipeline() - } - } - } -} - -func (sdk *AppFunctionsSDK) processConfigChangedStoreForwardRetryInterval() { - if sdk.config.Writable.StoreAndForward.Enabled { - sdk.stopStoreForward() - sdk.startStoreForward() - } -} - -func (sdk *AppFunctionsSDK) processConfigChangedStoreForwardEnabled() { - if sdk.config.Writable.StoreAndForward.Enabled { - // StoreClient must be set up for StoreAndForward - if sdk.storeClient == nil { - if sdk.initializeStoreClient() != nil { - // Error already logged - sdk.config.Writable.StoreAndForward.Enabled = false - return - } - - sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) - } - - sdk.startStoreForward() - } else { - sdk.stopStoreForward() - } -} - -func (sdk *AppFunctionsSDK) processConfigChangedPipeline() { - if sdk.usingConfigurablePipeline { - transforms, err := sdk.LoadConfigurablePipeline() - if err != nil { - sdk.LoggingClient.Error("unable to reload Configurable Pipeline from Registry: " + err.Error()) - return - } - err = sdk.SetFunctionsPipeline(transforms...) - if err != nil { - sdk.LoggingClient.Error("unable to set Configurable Pipeline from Registry: " + err.Error()) - return - } - - sdk.LoggingClient.Info("Reloaded Configurable Pipeline from Registry") - } -} - -func (sdk *AppFunctionsSDK) startStoreForward() { - var storeForwardEnabledCtx context.Context - sdk.storeForwardWg = &sync.WaitGroup{} - storeForwardEnabledCtx, sdk.storeForwardCancelCtx = context.WithCancel(context.Background()) - sdk.runtime.StartStoreAndForward(sdk.appWg, sdk.appCtx, - sdk.storeForwardWg, storeForwardEnabledCtx, - sdk.ServiceKey, &sdk.config, sdk.edgexClients) -} - -func (sdk *AppFunctionsSDK) stopStoreForward() { - sdk.LoggingClient.Info("Canceling Store and Forward retry loop") - sdk.storeForwardCancelCtx() - sdk.storeForwardWg.Wait() -} - -// GetSecrets retrieves secrets from a secret store. -// path specifies the type or location of the secrets to retrieve. If specified it is appended -// to the base path from the SecretConfig -// keys specifies the secrets which to retrieve. If no keys are provided then all the keys associated with the -// specified path will be returned. -func (sdk *AppFunctionsSDK) GetSecrets(path string, keys ...string) (map[string]string, error) { - return sdk.secretProvider.GetSecrets(path, keys...) -} - -// StoreSecrets stores the secrets to a secret store. -// it sets the values requested at provided keys -// path specifies the type or location of the secrets to store. If specified it is appended -// to the base path from the SecretConfig -// secrets map specifies the "key": "value" pairs of secrets to store -func (sdk *AppFunctionsSDK) StoreSecrets(path string, secrets map[string]string) error { - return sdk.secretProvider.StoreSecrets(path, secrets) -} - -func (sdk *AppFunctionsSDK) setLoggingTarget() (string, error) { - if sdk.config.Logging.EnableRemote { - logging, ok := sdk.config.Clients[common.LoggingClientName] - if !ok { - return "", errors.New("logging client configuration is missing") - } - - return logging.Url() + clients.ApiLoggingRoute, nil - } - - return sdk.config.Logging.File, nil -} - -func applyCommandlineEnvironmentOverrides() { - // Currently there is just one commandline option that can be overwritten with an environment variable. - // If more are added, a more dynamic data driven approach should be used to avoid code duplication. - - profileName := os.Getenv(ProfileEnvironmentVariable) - if profileName == "" { - return - } - - found := false - for index, option := range os.Args { - if strings.Contains(option, "-p=") || strings.Contains(option, "--profile=") { - os.Args[index] = "--profile=" + profileName - found = true - } - } - - if !found { - os.Args = append(os.Args, "--profile="+profileName) - } -} - -func readConfigurationFromFile(profileName string, configDir string) (*common.ConfigurationStruct, error) { - configuration, err := common.LoadFromFile(profileName, configDir) - if err != nil { - return nil, err +func (sdk *AppFunctionsSDK) addDeferred(deferred bootstrap.Deferred) { + if deferred != nil { + sdk.deferredFunctions = append(sdk.deferredFunctions, deferred) } - return configuration, nil } diff --git a/appsdk/sdk_test.go b/appsdk/sdk_test.go index 6f1d7c66c..1456565ed 100644 --- a/appsdk/sdk_test.go +++ b/appsdk/sdk_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,28 +17,23 @@ package appsdk import ( - "fmt" "net/http" - "net/http/httptest" - "net/url" "reflect" - "strconv" "testing" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/models" + "github.com/edgexfoundry/app-functions-sdk-go/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/internal" "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" triggerHttp "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/internal/webserver" - - "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/models" ) var lc logger.LoggingClient @@ -74,7 +69,7 @@ func TestAddRoute(t *testing.T) { func TestSetupHTTPTrigger(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Binding: common.BindingInfo{ Type: "htTp", }, @@ -91,7 +86,7 @@ func TestSetupHTTPTrigger(t *testing.T) { func TestSetupMessageBusTrigger(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Binding: common.BindingInfo{ Type: "meSsaGebus", }, @@ -108,7 +103,7 @@ func TestSetupMessageBusTrigger(t *testing.T) { func TestSetFunctionsPipelineNoTransforms(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Binding: common.BindingInfo{ Type: "meSsaGebus", }, @@ -123,7 +118,7 @@ func TestSetFunctionsPipelineOneTransform(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, runtime: &runtime.GolangRuntime{}, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Binding: common.BindingInfo{ Type: "meSsaGebus", }, @@ -144,7 +139,7 @@ func TestApplicationSettings(t *testing.T) { expectedSettingValue := "simple-filter-xml" sdk := AppFunctionsSDK{ - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "ApplicationName": "simple-filter-xml", }, @@ -161,7 +156,7 @@ func TestApplicationSettings(t *testing.T) { func TestApplicationSettingsNil(t *testing.T) { sdk := AppFunctionsSDK{ - config: common.ConfigurationStruct{}, + config: &common.ConfigurationStruct{}, } appSettings := sdk.ApplicationSettings() @@ -173,7 +168,7 @@ func TestGetAppSettingStrings(t *testing.T) { expected := []string{"dev1", "dev2"} sdk := AppFunctionsSDK{ - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "DeviceNames": "dev1, dev2", }, @@ -190,7 +185,7 @@ func TestGetAppSettingStringsSettingMissing(t *testing.T) { expected := "setting not found in ApplicationSettings" sdk := AppFunctionsSDK{ - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{}, }, } @@ -205,7 +200,7 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { expected := "ApplicationSettings section is missing" sdk := AppFunctionsSDK{ - config: common.ConfigurationStruct{}, + config: &common.ConfigurationStruct{}, } _, err := sdk.GetAppSettingStrings(setting) @@ -216,7 +211,7 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { func TestLoadConfigurablePipelineFunctionNotFound(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: "Bogus", @@ -238,7 +233,7 @@ func TestLoadConfigurablePipelineNotABuiltInSdkFunction(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: "Bogus", @@ -270,7 +265,7 @@ func TestLoadConfigurablePipelineAddressableConfig(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: functionName, @@ -295,7 +290,7 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: "FilterByDeviceName, TransformToXML, SetOutputData", @@ -318,7 +313,7 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: "CompressWithGZIP, SetOutputData", @@ -343,7 +338,7 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { sdk := AppFunctionsSDK{ LoggingClient: lc, - config: common.ConfigurationStruct{ + config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ ExecutionOrder: "CompressWithGZIP, SetOutputData", @@ -358,150 +353,3 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { require.NoError(t, err) assert.Nil(t, sdk.TargetType) } - -func TestSetLoggingTargetLocal(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - config: common.ConfigurationStruct{ - Logging: common.LoggingInfo{ - EnableRemote: false, - File: "./myfile", - }, - }, - } - result, err := sdk.setLoggingTarget() - require.NoError(t, err) - assert.Equal(t, "./myfile", result, "File path is incorrect") -} - -func TestSetLoggingTargetRemote(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - config: common.ConfigurationStruct{ - Clients: map[string]common.ClientInfo{ - "Logging": { - Protocol: "http", - Host: "logs", - Port: 8080, - }, - }, - Logging: common.LoggingInfo{ - EnableRemote: true, - }, - }, - } - result, err := sdk.setLoggingTarget() - require.NoError(t, err) - assert.Equal(t, "http://logs:8080/api/v1/logs", result, "File path is incorrect") -} - -func TestInitializeClientsAll(t *testing.T) { - coreClients := make(map[string]common.ClientInfo) - coreClients[common.CoreDataClientName] = common.ClientInfo{} - coreClients[common.NotificationsClientName] = common.ClientInfo{} - coreClients[common.CoreCommandClientName] = common.ClientInfo{} - - sdk := AppFunctionsSDK{ - LoggingClient: lc, - config: common.ConfigurationStruct{ - Clients: coreClients, - }, - } - - sdk.initializeClients() - - assert.NotNil(t, sdk.edgexClients.EventClient) - assert.NotNil(t, sdk.edgexClients.CommandClient) - assert.NotNil(t, sdk.edgexClients.ValueDescriptorClient) - assert.NotNil(t, sdk.edgexClients.NotificationsClient) -} - -func TestInitializeClientsJustData(t *testing.T) { - coreClients := make(map[string]common.ClientInfo) - coreClients[common.CoreDataClientName] = common.ClientInfo{} - - sdk := AppFunctionsSDK{ - LoggingClient: lc, - config: common.ConfigurationStruct{ - Clients: coreClients, - }, - } - - sdk.initializeClients() - - assert.NotNil(t, sdk.edgexClients.EventClient) - assert.NotNil(t, sdk.edgexClients.ValueDescriptorClient) - - assert.Nil(t, sdk.edgexClients.CommandClient) - assert.Nil(t, sdk.edgexClients.NotificationsClient) -} - -func TestValidateVersionMatch(t *testing.T) { - coreClients := make(map[string]common.ClientInfo) - coreClients[common.CoreDataClientName] = common.ClientInfo{ - Protocol: "http", - Host: "localhost", - Port: 0, // Will be replaced by local test webserver's port - } - - sdk := AppFunctionsSDK{ - LoggingClient: lc, - config: common.ConfigurationStruct{ - Clients: coreClients, - }, - } - - tests := []struct { - Name string - CoreVersion string - SdkVersion string - skipVersionCheck bool - ExpectFailure bool - }{ - {"Compatible Versions", "1.1.0", "v1.0.0", false, false}, - {"Dev Compatible Versions", "2.0.0", "v2.0.0-dev.11", false, false}, - {"Un-compatible Versions", "2.0.0", "v1.0.0", false, true}, - {"Skip Version Check", "2.0.0", "v1.0.0", true, false}, - {"Running in Debugger", "1.0.0", "v0.0.0", false, false}, - {"SDK Beta Version", "1.0.0", "v0.2.0", false, false}, - {"SDK Version malformed", "1.0.0", "", false, true}, - {"Core version malformed", "12", "v1.0.0", false, true}, - {"Core version JSON bad", "", "v1.0.0", false, true}, - {"Core version JSON empty", "{}", "v1.0.0", false, true}, - } - - for _, test := range tests { - t.Run(test.Name, func(t *testing.T) { - - internal.SDKVersion = test.SdkVersion - sdk.skipVersionCheck = test.skipVersionCheck - - handler := func(w http.ResponseWriter, r *http.Request) { - var versionJson string - if test.CoreVersion == "{}" { - versionJson = "{}" - } else if test.CoreVersion == "" { - versionJson = "" - } else { - versionJson = fmt.Sprintf(`{"version" : "%s"}`, test.CoreVersion) - } - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(versionJson)) - } - - // create test server with handler - testServer := httptest.NewServer(http.HandlerFunc(handler)) - defer testServer.Close() - - testServerUrl, _ := url.Parse(testServer.URL) - port, _ := strconv.Atoi(testServerUrl.Port()) - coreService := sdk.config.Clients[common.CoreDataClientName] - coreService.Port = port - sdk.config.Clients[common.CoreDataClientName] = coreService - - result := sdk.validateVersionMatch() - assert.Equal(t, test.ExpectFailure, !result) - }) - } -} diff --git a/bin/test-attribution-txt.sh b/bin/test-attribution-txt.sh deleted file mode 100755 index 2bc890758..000000000 --- a/bin/test-attribution-txt.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -e - -# get the directory of this script -# snippet from https://stackoverflow.com/a/246128/10102404 -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -GIT_ROOT=$(dirname "$SCRIPT_DIR") - -EXIT_CODE=0 - -cd "$GIT_ROOT" - -if [ -d vendor.bk ]; then - echo "vendor.bk exits - remove before continuing" - exit 1 -fi - -trap cleanup 1 2 3 6 - -cleanup() -{ - cd "$GIT_ROOT" - # restore the vendor dir - rm -r vendor - if [ -d vendor.bk ]; then - mv vendor.bk vendor - fi - exit $EXIT_CODE -} - -# if the vendor directory exists, back it up so we can build a fresh one -if [ -d vendor ]; then - mv vendor vendor.bk -fi - -# create a vendor dir with the mod dependencies -GO111MODULE=on go mod vendor - -# turn on nullglobbing so if there is nothing in cmd dir then we don't do -# anything in this loop -shopt -s nullglob - - -if [ ! -f Attribution.txt ]; then - echo "An Attribution.txt file for $cmd is missing, please add" - EXIT_CODE=1 -else - # loop over every library in the modules.txt file in vendor - while IFS= read -r lib; do - if ! grep -q "$lib" Attribution.txt; then - echo "An attribution for $lib is missing from in $cmd Attribution.txt, please add" - # need to do this in a bash subshell, see SC2031 - (( EXIT_CODE=1 )) - fi - done < <(grep '#' < "$GIT_ROOT/vendor/modules.txt" | awk '{print $2}') -fi - -cd "$GIT_ROOT" - -cleanup diff --git a/go.mod b/go.mod index 7e4e20657..ee70a10b3 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,12 @@ go 1.13 require ( bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690 - github.com/BurntSushi/toml v0.3.1 github.com/diegoholiveira/jsonlogic v1.0.1-0.20200220175622-ab7989be08b9 - github.com/eclipse/paho.mqtt.golang v1.2.0 - github.com/edgexfoundry/go-mod-core-contracts v0.1.52 - github.com/edgexfoundry/go-mod-messaging v0.1.16 - github.com/edgexfoundry/go-mod-registry v0.1.11 + github.com/edgexfoundry/go-mod-bootstrap v0.0.29 + github.com/edgexfoundry/go-mod-core-contracts v0.1.57 + github.com/edgexfoundry/go-mod-messaging v0.1.18 + github.com/edgexfoundry/go-mod-registry v0.1.20 github.com/edgexfoundry/go-mod-secrets v0.0.17 github.com/golang/snappy v0.0.1 // indirect github.com/gomodule/redigo v2.0.0+incompatible @@ -18,9 +17,8 @@ require ( github.com/google/uuid v1.1.0 github.com/gorilla/mux v1.7.2 github.com/kr/pretty v0.2.0 // indirect - github.com/pelletier/go-toml v1.2.0 github.com/stretchr/objx v0.2.0 // indirect - github.com/stretchr/testify v1.4.0 + github.com/stretchr/testify v1.5.1 github.com/tidwall/pretty v1.0.0 // indirect github.com/ugorji/go v1.1.4 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect diff --git a/internal/bootstrap/container/clients.go b/internal/bootstrap/container/clients.go new file mode 100644 index 000000000..1fb43a346 --- /dev/null +++ b/internal/bootstrap/container/clients.go @@ -0,0 +1,72 @@ +// +// Copyright (c) 2020 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 container + +import ( + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-core-contracts/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/clients/notifications" + + "github.com/edgexfoundry/go-mod-core-contracts/clients/coredata" +) + +// ValueDescriptorClientName contains the name of the ValueDescriptorClient's implementation in the DIC. +var ValueDescriptorClientName = di.TypeInstanceToName((*coredata.ValueDescriptorClient)(nil)) + +// ValueDescriptorClientFrom helper function queries the DIC and returns the ValueDescriptorClient's implementation. +func ValueDescriptorClientFrom(get di.Get) coredata.ValueDescriptorClient { + if get(ValueDescriptorClientName) == nil { + return nil + } + + return get(ValueDescriptorClientName).(coredata.ValueDescriptorClient) +} + +// EventClientName contains the name of the EventClient's implementation in the DIC. +var EventClientName = di.TypeInstanceToName((*coredata.EventClient)(nil)) + +// ValueDescriptorClientFrom helper function queries the DIC and returns the ValueDescriptorClient's implementation. +func EventClientFrom(get di.Get) coredata.EventClient { + if get(EventClientName) == nil { + return nil + } + + return get(EventClientName).(coredata.EventClient) +} + +// NotificationsClientName contains the name of the NotificationsClientInfo's implementation in the DIC. +var NotificationsClientName = di.TypeInstanceToName((*notifications.NotificationsClient)(nil)) + +// NotificationsClientFrom helper function queries the DIC and returns the NotificationsClientInfo's implementation. +func NotificationsClientFrom(get di.Get) notifications.NotificationsClient { + if get(NotificationsClientName) == nil { + return nil + } + + return get(NotificationsClientName).(notifications.NotificationsClient) +} + +// CommandClientName contains the name of the CommandClientInfo's implementation in the DIC. +var CommandClientName = di.TypeInstanceToName((*command.CommandClient)(nil)) + +// NotificationsClientFrom helper function queries the DIC and returns the NotificationsClientInfo's implementation. +func CommandClientFrom(get di.Get) command.CommandClient { + if get(CommandClientName) == nil { + return nil + } + + return get(CommandClientName).(command.CommandClient) +} diff --git a/internal/bootstrap/container/config.go b/internal/bootstrap/container/config.go new file mode 100644 index 000000000..8fb57be60 --- /dev/null +++ b/internal/bootstrap/container/config.go @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 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 container + +import ( + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/go-mod-bootstrap/di" +) + +// ConfigurationName contains the name of data's common.ConfigurationStruct implementation in the DIC. +var ConfigurationName = di.TypeInstanceToName(common.ConfigurationStruct{}) + +// ConfigurationFrom helper function queries the DIC and returns datas's common.ConfigurationStruct implementation. +func ConfigurationFrom(get di.Get) *common.ConfigurationStruct { + return get(ConfigurationName).(*common.ConfigurationStruct) +} diff --git a/internal/bootstrap/container/secretprovider.go b/internal/bootstrap/container/secretprovider.go new file mode 100644 index 000000000..432e8b0e6 --- /dev/null +++ b/internal/bootstrap/container/secretprovider.go @@ -0,0 +1,30 @@ +// +// Copyright (c) 2020 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 container + +import ( + "github.com/edgexfoundry/go-mod-bootstrap/di" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" +) + +// SecretProviderName contains the name of the security.SecretProvider implementation in the DIC. +var SecretProviderName = di.TypeInstanceToName(&security.SecretProvider{}) + +// SecretProviderFrom helper function queries the DIC and returns the security.SecretProvider implementation. +func SecretProviderFrom(get di.Get) *security.SecretProvider { + return get(SecretProviderName).(*security.SecretProvider) +} diff --git a/internal/bootstrap/container/storeclient.go b/internal/bootstrap/container/storeclient.go new file mode 100644 index 000000000..16208dd03 --- /dev/null +++ b/internal/bootstrap/container/storeclient.go @@ -0,0 +1,36 @@ +// +// Copyright (c) 2020 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 container + +import ( + "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db/interfaces" + + "github.com/edgexfoundry/go-mod-bootstrap/di" +) + +// StoreClientName contains the name of interfaces.StoreClient implementation in the DIC. +var StoreClientName = di.TypeInstanceToName((*interfaces.StoreClient)(nil)) + +// StoreClientFrom helper function queries the DIC and returns interfaces.StoreClient implementation. +func StoreClientFrom(get di.Get) interfaces.StoreClient { + item := get(StoreClientName) + + if item == nil { + return nil + } + + return item.(interfaces.StoreClient) +} diff --git a/internal/bootstrap/handlers/clients.go b/internal/bootstrap/handlers/clients.go new file mode 100644 index 000000000..a7b2b5b4c --- /dev/null +++ b/internal/bootstrap/handlers/clients.go @@ -0,0 +1,154 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "fmt" + "sync" + "time" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-core-contracts/clients" + "github.com/edgexfoundry/go-mod-core-contracts/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/clients/notifications" + + "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/pkg/urlclient" +) + +// Clients contains references to dependencies required by the Clients bootstrap implementation. +type Clients struct { +} + +// NewClients create a new instance of Clients +func NewClients() *Clients { + return &Clients{} +} + +// BootstrapHandler setups all the clients that have be specified in the configuration +func (_ *Clients) BootstrapHandler( + ctx context.Context, + wg *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + logger := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + registryClient := bootstrapContainer.RegistryFrom(dic.Get) + + var eventClient coredata.EventClient + var valueDescriptorClient coredata.ValueDescriptorClient + var commandClient command.CommandClient + var notificationsClient notifications.NotificationsClient + + // Need when passing all Clients to other components + clientMonitor, err := time.ParseDuration(config.Service.ClientMonitor) + if err != nil { + logger.Warn( + fmt.Sprintf( + "Service.ClientMonitor failed to parse: %s, use the default value: %v", + err, + internal.ClientMonitorDefault, + ), + ) + // fall back to default value + clientMonitor = internal.ClientMonitorDefault + } + + interval := int(clientMonitor / time.Millisecond) + + // Use of these client interfaces is optional, so they are not required to be configured. For instance if not + // sending commands, then don't need to have the Command client in the configuration. + if _, ok := config.Clients[common.CoreDataClientName]; ok { + eventClient = coredata.NewEventClient( + urlclient.New( + context.Background(), + wg, + registryClient, + clients.CoreDataServiceKey, + clients.ApiEventRoute, + interval, + config.Clients[common.CoreDataClientName].Url()+clients.ApiEventRoute, + ), + ) + + valueDescriptorClient = coredata.NewValueDescriptorClient( + urlclient.New( + context.Background(), + wg, + registryClient, + clients.CoreDataServiceKey, + clients.ApiValueDescriptorRoute, + interval, + config.Clients[common.CoreDataClientName].Url()+clients.ApiValueDescriptorRoute, + ), + ) + } + + if _, ok := config.Clients[common.CoreCommandClientName]; ok { + commandClient = command.NewCommandClient( + urlclient.New( + context.Background(), + wg, + registryClient, + clients.CoreCommandServiceKey, + clients.ApiDeviceRoute, + interval, + config.Clients[common.CoreCommandClientName].Url()+clients.ApiDeviceRoute, + ), + ) + } + + if _, ok := config.Clients[common.NotificationsClientName]; ok { + notificationsClient = notifications.NewNotificationsClient( + urlclient.New( + context.Background(), + wg, + registryClient, + clients.SupportNotificationsServiceKey, + clients.ApiNotificationRoute, + interval, + config.Clients[common.NotificationsClientName].Url()+clients.ApiNotificationRoute, + ), + ) + } + + // Note that all the clients are optional so some or all these clients may be nil + // Code that uses them must verify the client was defined and created prior to using it. + // This information is provided in the documentation. + dic.Update(di.ServiceConstructorMap{ + container.EventClientName: func(get di.Get) interface{} { + return eventClient + }, + container.ValueDescriptorClientName: func(get di.Get) interface{} { + return valueDescriptorClient + }, + container.CommandClientName: func(get di.Get) interface{} { + return commandClient + }, + container.NotificationsClientName: func(get di.Get) interface{} { + return notificationsClient + }, + }) + + return true +} diff --git a/internal/bootstrap/handlers/clients_test.go b/internal/bootstrap/handlers/clients_test.go new file mode 100644 index 000000000..3d96717aa --- /dev/null +++ b/internal/bootstrap/handlers/clients_test.go @@ -0,0 +1,174 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "sync" + "testing" + + "github.com/edgexfoundry/go-mod-registry/registry" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/di" + + "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" +) + +func TestClientsBootstrapHandler(t *testing.T) { + configuration := &common.ConfigurationStruct{ + Service: common.ServiceInfo{ + ClientMonitor: "5s", + }, + } + + logger := logging.FactoryToStdout("clients-test") + var registryClient registry.Client = nil + + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger + }, + bootstrapContainer.RegistryClientInterfaceName: func(get di.Get) interface{} { + return registryClient + }, + }) + + coreDataClientInfo := config.ClientInfo{ + Host: "localhost", + Port: 48080, + Protocol: "http", + } + + commandClientInfo := config.ClientInfo{ + Host: "localhost", + Port: 48081, + Protocol: "http", + } + + notificationsClientInfo := config.ClientInfo{ + Host: "localhost", + Port: 48082, + Protocol: "http", + } + + startupTimer := startup.NewStartUpTimer(internal.BootRetrySecondsDefault, internal.BootTimeoutSecondsDefault) + + tests := []struct { + Name string + CoreDataClientInfo *config.ClientInfo + CommandClientInfo *config.ClientInfo + NotificationsClientInfo *config.ClientInfo + MonitorDuration string + ExpectSuccess bool + }{ + { + Name: "All Clients", + CoreDataClientInfo: &coreDataClientInfo, + CommandClientInfo: &commandClientInfo, + NotificationsClientInfo: ¬ificationsClientInfo, + MonitorDuration: "5s", + ExpectSuccess: true, + }, + { + Name: "No Clients", + CoreDataClientInfo: nil, + CommandClientInfo: nil, + NotificationsClientInfo: nil, + MonitorDuration: "5s", + ExpectSuccess: true, + }, + { + Name: "Only Core Data Clients", + CoreDataClientInfo: &coreDataClientInfo, + CommandClientInfo: nil, + NotificationsClientInfo: nil, + MonitorDuration: "5s", + ExpectSuccess: true, + }, + { + Name: "Invalid MonitorDuration", + CoreDataClientInfo: &coreDataClientInfo, + CommandClientInfo: nil, + NotificationsClientInfo: nil, + MonitorDuration: "bogus", + ExpectSuccess: true, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + configuration.Service.ClientMonitor = test.MonitorDuration + configuration.Clients = make(map[string]config.ClientInfo) + + if test.CoreDataClientInfo != nil { + configuration.Clients[common.CoreDataClientName] = coreDataClientInfo + } + + if test.CommandClientInfo != nil { + configuration.Clients[common.CoreCommandClientName] = commandClientInfo + } + + if test.NotificationsClientInfo != nil { + configuration.Clients[common.NotificationsClientName] = notificationsClientInfo + } + + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return configuration + }, + }) + + actualSuccess := NewClients().BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) + require.Equal(t, test.ExpectSuccess, actualSuccess) + if actualSuccess == false { + return // Test is complete + } + + eventClient := container.EventClientFrom(dic.Get) + valueDescriptorClient := container.ValueDescriptorClientFrom(dic.Get) + commandClient := container.CommandClientFrom(dic.Get) + notificationsClient := container.NotificationsClientFrom(dic.Get) + + if test.CoreDataClientInfo != nil { + assert.NotNil(t, eventClient) + assert.NotNil(t, valueDescriptorClient) + } else { + assert.Nil(t, eventClient) + assert.Nil(t, valueDescriptorClient) + } + + if test.CommandClientInfo != nil { + assert.NotNil(t, commandClient) + } else { + assert.Nil(t, commandClient) + } + + if test.NotificationsClientInfo != nil { + assert.NotNil(t, notificationsClient) + } else { + assert.Nil(t, notificationsClient) + } + }) + } +} diff --git a/internal/bootstrap/handlers/secrets.go b/internal/bootstrap/handlers/secrets.go new file mode 100644 index 000000000..6c53a36de --- /dev/null +++ b/internal/bootstrap/handlers/secrets.go @@ -0,0 +1,63 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "sync" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" +) + +// Secrets contains references to dependencies required by the Secrets bootstrap implementation. +type Secrets struct { +} + +// NewDatabase create a new instance of Database +func NewSecrets() *Secrets { + return &Secrets{} +} + +// BootstrapHandler creates the SecretProvider based on configuration. +func (_ *Secrets) BootstrapHandler( + ctx context.Context, + _ *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + logger := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + + secretProvider := security.NewSecretProvider(logger, config) + ok := secretProvider.Initialize(ctx) + if !ok { + logger.Error("unable to initialize secret provider") + return false + } + + dic.Update(di.ServiceConstructorMap{ + container.SecretProviderName: func(get di.Get) interface{} { + return secretProvider + }, + }) + + return true +} diff --git a/internal/bootstrap/handlers/storeclient.go b/internal/bootstrap/handlers/storeclient.go new file mode 100644 index 000000000..2c665c12a --- /dev/null +++ b/internal/bootstrap/handlers/storeclient.go @@ -0,0 +1,101 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "fmt" + "sync" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security" + "github.com/edgexfoundry/app-functions-sdk-go/internal/store" + "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db/interfaces" +) + +// Database contains references to dependencies required by the database bootstrap implementation. +type Database struct { +} + +// NewDatabase create a new instance of Database +func NewDatabase() *Database { + return &Database{} +} + +// BootstrapHandler creates the new interfaces.StoreClient use for database access by Store & Forward capability +func (_ *Database) BootstrapHandler( + ctx context.Context, + _ *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + config := container.ConfigurationFrom(dic.Get) + + // Only need the database client if Store and Forward is enabled + if !config.Writable.StoreAndForward.Enabled { + dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return nil + }, + }) + return true + } + + logger := bootstrapContainer.LoggingClientFrom(dic.Get) + secretProvider := container.SecretProviderFrom(dic.Get) + + storeClient, err := InitializeStoreClient(secretProvider, config) + if err != nil { + logger.Error(err.Error()) + return false + } + + dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return storeClient + }, + }) + + return true +} + +// InitializeStoreClient initializes the database client for Store and Forward. This is not a receiver function so that +// it can be called directly when configuration has changed and store and forward has been enabled for the first time +func InitializeStoreClient( + secretProvider *security.SecretProvider, + config *common.ConfigurationStruct) (interfaces.StoreClient, error) { + var err error + + credentials, err := secretProvider.GetDatabaseCredentials(config.Database) + if err != nil { + return nil, fmt.Errorf("unable to get Database Credentials for Store and Forward: %s", err.Error()) + } + + config.Database.Username = credentials.Username + config.Database.Password = credentials.Password + + storeClient, err := store.NewStoreClient(config.Database) + if err != nil { + return nil, fmt.Errorf("unable to initialize Database for Store and Forward: %s", err.Error()) + } + + return storeClient, err +} diff --git a/internal/bootstrap/handlers/telemetry.go b/internal/bootstrap/handlers/telemetry.go new file mode 100644 index 000000000..e61711b51 --- /dev/null +++ b/internal/bootstrap/handlers/telemetry.go @@ -0,0 +1,51 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "sync" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/telemetry" +) + +// Telemetry contains references to dependencies required by the Telemetry bootstrap implementation. +type Telemetry struct { +} + +// New Telemetry create a new instance of Telemetry +func NewTelemetry() *Telemetry { + return &Telemetry{} +} + +// BootstrapHandler starts the telemetry collection +func (_ *Telemetry) BootstrapHandler( + ctx context.Context, + wg *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + logger := container.LoggingClientFrom(dic.Get) + + wg.Add(1) + go telemetry.StartCpuUsageAverage(wg, ctx, logger) + + return true +} diff --git a/internal/bootstrap/handlers/version.go b/internal/bootstrap/handlers/version.go new file mode 100644 index 000000000..0559a831e --- /dev/null +++ b/internal/bootstrap/handlers/version.go @@ -0,0 +1,137 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-core-contracts/clients" + + "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" +) + +const ( + CorePreReleaseVersion = "master" + CoreServiceVersionKey = "version" + VersionMajorIndex = 0 +) + +// VersionValidator contains references to dependencies required by the Version Validation bootstrap implementation. +type VersionValidator struct { + skipVersionCheck bool + sdkVersion string +} + +// NewVersionValidator create a new instance of VersionValidator +func NewVersionValidator(skip bool, sdkVersion string) *VersionValidator { + return &VersionValidator{ + skipVersionCheck: skip, + sdkVersion: sdkVersion, + } +} + +// BootstrapHandler verifies that Core Services major version matches this SDK's major version +func (vv *VersionValidator) BootstrapHandler( + ctx context.Context, + wg *sync.WaitGroup, + startupTimer startup.Timer, + dic *di.Container) bool { + + logger := bootstrapContainer.LoggingClientFrom(dic.Get) + config := container.ConfigurationFrom(dic.Get) + + if vv.skipVersionCheck { + logger.Info("Skipping core service version compatibility check") + return true + } + + // SDK version is set via the SemVer TAG at build time + // and has the format "v{major}.{minor}.{patch}[-dev.{build}]" + sdkVersionParts := strings.Split(vv.sdkVersion, ".") + if len(sdkVersionParts) < 3 { + logger.Error("SDK version is malformed", "version", internal.SDKVersion) + return false + } + + sdkVersionParts[VersionMajorIndex] = strings.Replace(sdkVersionParts[VersionMajorIndex], "v", "", 1) + if sdkVersionParts[VersionMajorIndex] == "0" { + logger.Info( + "Skipping version compatibility check for SDK Beta version or running in debugger", + "version", + internal.SDKVersion) + return true + } + + url := config.Clients[common.CoreDataClientName].Url() + clients.ApiVersionRoute + data, err := clients.GetRequestWithURL(context.Background(), url) + if err != nil { + logger.Error("Unable to get version of Core Services", "error", err) + return false + } + + versionJson := map[string]string{} + err = json.Unmarshal(data, &versionJson) + if err != nil { + logger.Error("Unable to un-marshal Core Services version data", "error", err) + return false + } + + coreVersion, ok := versionJson[CoreServiceVersionKey] + if !ok { + logger.Error(fmt.Sprintf("Core Services version data missing '%s' information", CoreServiceVersionKey)) + return false + } + + if coreVersion == CorePreReleaseVersion { + logger.Info( + "Skipping version compatibility check for Core Pre-release version", + "version", + internal.SDKVersion) + return true + } + + // Core Service version is reported as "{major}.{minor}.{patch}" + coreVersionParts := strings.Split(coreVersion, ".") + if len(coreVersionParts) != 3 { + logger.Error("Core Services version is malformed", "version", coreVersion) + return false + } + + // Do Major versions match? + if coreVersionParts[0] == sdkVersionParts[0] { + logger.Debug( + fmt.Sprintf("Confirmed Core Services version (%s) is compatible with SDK's version (%s)", + coreVersion, + internal.SDKVersion)) + return true + } + + logger.Error( + fmt.Sprintf("Core services version (%s) is not compatible with SDK's version(%s)", + coreVersion, + internal.SDKVersion)) + + return false +} diff --git a/internal/bootstrap/handlers/version_test.go b/internal/bootstrap/handlers/version_test.go new file mode 100644 index 000000000..77ed2c85c --- /dev/null +++ b/internal/bootstrap/handlers/version_test.go @@ -0,0 +1,125 @@ +// +// Copyright (c) 2020 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 handlers + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "sync" + "testing" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/logging" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/go-mod-bootstrap/di" + "github.com/edgexfoundry/go-mod-registry/registry" + "github.com/stretchr/testify/assert" + + "github.com/edgexfoundry/app-functions-sdk-go/internal" + "github.com/edgexfoundry/app-functions-sdk-go/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" +) + +func TestValidateVersionMatch(t *testing.T) { + startupTimer := startup.NewStartUpTimer(internal.BootRetrySecondsDefault, internal.BootTimeoutSecondsDefault) + + clients := make(map[string]config.ClientInfo) + clients[common.CoreDataClientName] = config.ClientInfo{ + Protocol: "http", + Host: "localhost", + Port: 0, // Will be replaced by local test webserver's port + } + + configuration := &common.ConfigurationStruct{ + Writable: common.WritableInfo{ + LogLevel: "DEBUG", + }, + Clients: clients, + } + + logger := logging.FactoryToStdout("clients-test") + var registryClient registry.Client = nil + + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger + }, + bootstrapContainer.RegistryClientInterfaceName: func(get di.Get) interface{} { + return registryClient + }, + container.ConfigurationName: func(get di.Get) interface{} { + return configuration + }, + }) + + tests := []struct { + Name string + CoreVersion string + SdkVersion string + skipVersionCheck bool + ExpectFailure bool + }{ + {"Compatible Versions", "1.1.0", "v1.0.0", false, false}, + {"Dev Compatible Versions", "2.0.0", "v2.0.0-dev.11", false, false}, + {"Un-compatible Versions", "2.0.0", "v1.0.0", false, true}, + {"Skip Version Check", "2.0.0", "v1.0.0", true, false}, + {"Running in Debugger", "1.0.0", "v0.0.0", false, false}, + {"SDK Beta Version", "1.0.0", "v0.2.0", false, false}, + {"SDK Version malformed", "1.0.0", "", false, true}, + {"Core prerelease version", CorePreReleaseVersion, "v1.0.0", false, false}, + {"Core version malformed", "12", "v1.0.0", false, true}, + {"Core version JSON bad", "", "v1.0.0", false, true}, + {"Core version JSON empty", "{}", "v1.0.0", false, true}, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + + handler := func(w http.ResponseWriter, r *http.Request) { + var versionJson string + if test.CoreVersion == "{}" { + versionJson = "{}" + } else if test.CoreVersion == "" { + versionJson = "" + } else { + versionJson = fmt.Sprintf(`{"version" : "%s"}`, test.CoreVersion) + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(versionJson)) + } + + // create test server with handler + testServer := httptest.NewServer(http.HandlerFunc(handler)) + defer testServer.Close() + + testServerUrl, _ := url.Parse(testServer.URL) + port, _ := strconv.Atoi(testServerUrl.Port()) + coreService := configuration.Clients[common.CoreDataClientName] + coreService.Port = port + configuration.Clients[common.CoreDataClientName] = coreService + + validator := NewVersionValidator(test.skipVersionCheck, test.SdkVersion) + result := validator.BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) + assert.Equal(t, test.ExpectFailure, !result) + }) + } +} diff --git a/internal/common/clients.go b/internal/common/clients.go index e33cc38c1..432f00abf 100644 --- a/internal/common/clients.go +++ b/internal/common/clients.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import ( ) const ( - LoggingClientName = "Logging" CoreCommandClientName = "Command" CoreDataClientName = "CoreData" NotificationsClientName = "Notifications" diff --git a/internal/common/config.go b/internal/common/config.go index b3f0e813f..1bc4479af 100644 --- a/internal/common/config.go +++ b/internal/common/config.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,12 +17,15 @@ package common import ( - "fmt" + "time" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap/interfaces" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" - "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db" "github.com/edgexfoundry/go-mod-core-contracts/models" "github.com/edgexfoundry/go-mod-messaging/pkg/types" - "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" + + "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db" ) // WritableInfo is used to hold configuration information that is considered "live" or can be changed on the fly without a restart of the service. @@ -38,30 +41,15 @@ type WritableInfo struct { InsecureSecrets InsecureSecrets } -// ClientInfo provides the host and port of another service in the eco-system. -type ClientInfo struct { - // Host is the hostname or IP address of a service. - Host string - // Port defines the port on which to access a given service - Port int - // Protocol indicates the protocol to use when accessing a given service - Protocol string -} - -func (c ClientInfo) Url() string { - url := fmt.Sprintf("%s://%s:%v", c.Protocol, c.Host, c.Port) - return url -} - // ConfigurationStruct // swagger:model ConfigurationStruct type ConfigurationStruct struct { // Writable Writable WritableInfo // Logging - Logging LoggingInfo + Logging bootstrapConfig.LoggingInfo // Registry - Registry RegistryInfo + Registry bootstrapConfig.RegistryInfo // Service Service ServiceInfo // MessageBus @@ -71,26 +59,15 @@ type ConfigurationStruct struct { // ApplicationSettings ApplicationSettings map[string]string // Clients - Clients map[string]ClientInfo + Clients map[string]bootstrapConfig.ClientInfo // Database Database db.DatabaseInfo // SecretStore - SecretStore SecretStoreInfo + SecretStore bootstrapConfig.SecretStoreInfo // SecretStoreExclusive - SecretStoreExclusive SecretStoreInfo -} - -// RegistryInfo is used for defining settings for connection to the registry. -type RegistryInfo struct { - Host string - Port int - Type string -} - -// LoggingInfo is used to indicate whether remote logging should be used or not. If not, File designates the location of the log file to output logs to -type LoggingInfo struct { - EnableRemote bool - File string + SecretStoreExclusive bootstrapConfig.SecretStoreInfo + // Startup + Startup bootstrapConfig.StartupInfo } // ServiceInfo is used to hold and configure various settings related to the hosting of this service @@ -138,13 +115,6 @@ type StoreAndForwardInfo struct { MaxRetryCount int } -// SecretStoreInfo encapsulates configuration properties used to create a SecretClient. -type SecretStoreInfo struct { - vault.SecretConfig - // TokenFile provides a location to a token file. - TokenFile string -} - // Credentials encapsulates username-password attributes. type Credentials struct { Username string @@ -159,3 +129,75 @@ type InsecureSecretsInfo struct { Path string Secrets map[string]string } + +// UpdateFromRaw converts configuration received from the registry to a service-specific configuration struct which is +// then used to overwrite the service's existing configuration struct. +func (c *ConfigurationStruct) UpdateFromRaw(rawConfig interface{}) bool { + configuration, ok := rawConfig.(*ConfigurationStruct) + if ok { + // Check that information was successfully read from Registry + if configuration.Service.Port == 0 { + return false + } + *c = *configuration + } + return ok +} + +// EmptyWritablePtr returns a pointer to an empty WritableInfo struct. It is used by the bootstrap to +// provide the appropriate structure for Config Client's WatchForChanges(). +func (c *ConfigurationStruct) EmptyWritablePtr() interface{} { + return &WritableInfo{} +} + +// UpdateWritableFromRaw updates the Writeable section of configuration from raw update received from Configuration Provider. +func (c *ConfigurationStruct) UpdateWritableFromRaw(rawWritable interface{}) bool { + writable, ok := rawWritable.(*WritableInfo) + if ok { + c.Writable = *writable + } + return ok +} + +// GetBootstrap returns the configuration elements required by the bootstrap. +func (c *ConfigurationStruct) GetBootstrap() interfaces.BootstrapConfiguration { + return interfaces.BootstrapConfiguration{ + Clients: c.Clients, + Service: c.transformToBootstrapServiceInfo(), + Registry: c.Registry, + Logging: c.Logging, + SecretStore: c.SecretStore, + Startup: c.Startup, + } +} + +// GetLogLevel returns log level from the configuration +func (c *ConfigurationStruct) GetLogLevel() string { + return c.Writable.LogLevel +} + +// GetRegistryInfo returns the RegistryInfo section from the configuration +func (c *ConfigurationStruct) GetRegistryInfo() bootstrapConfig.RegistryInfo { + return c.Registry +} + +// transformToBootstrapServiceInfo transforms the SDK's ServiceInfo to the bootstrap's version of ServiceInfo +func (c *ConfigurationStruct) transformToBootstrapServiceInfo() bootstrapConfig.ServiceInfo { + return bootstrapConfig.ServiceInfo{ + BootTimeout: durationToMill(c.Service.BootTimeout), + CheckInterval: c.Service.CheckInterval, + ClientMonitor: durationToMill(c.Service.ClientMonitor), + Host: c.Service.Host, + Port: c.Service.Port, + Protocol: c.Service.Protocol, + StartupMsg: c.Service.StartupMsg, + MaxResultCount: c.Service.ReadMaxLimit, + Timeout: durationToMill(c.Service.Timeout), + } +} + +// durationToMill converts a duration string to milliseconds integer value +func durationToMill(s string) int { + v, _ := time.ParseDuration(s) + return int(v.Milliseconds()) +} diff --git a/internal/common/loader.go b/internal/common/loader.go deleted file mode 100644 index 6b40af2e3..000000000 --- a/internal/common/loader.go +++ /dev/null @@ -1,70 +0,0 @@ -// -// 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 common - -import ( - "fmt" - "io/ioutil" - "os" - - "github.com/BurntSushi/toml" - - "github.com/edgexfoundry/app-functions-sdk-go/internal" -) - -const ( - configDirectory = "./res" - configDirEnv = "EDGEX_CONF_DIR" -) - -// LoadFromFile loads .toml file for configuration -func LoadFromFile(profile string, configDir string) (configuration *ConfigurationStruct, err error) { - path := determinePath(configDir) - fileName := path + "/" + internal.ConfigFileName //default profile - if len(profile) > 0 { - fileName = path + "/" + profile + "/" + internal.ConfigFileName - } - contents, err := ioutil.ReadFile(fileName) - if err != nil { - return nil, fmt.Errorf("Could not load configuration file (%s): %v", fileName, err.Error()) - } - - // Decode the configuration from TOML - configuration = &ConfigurationStruct{} - err = toml.Unmarshal(contents, configuration) - if err != nil { - return nil, fmt.Errorf("Unable to parse configuration file (%s): %v", fileName, err.Error()) - } - - return configuration, nil -} - -func determinePath(configDir string) string { - path := configDir - - if len(path) == 0 { //No cmd line param passed - //Assumption: one service per container means only one var is needed, set accordingly for each deployment. - //For local dev, do not set this variable since configs are all named the same. - path = os.Getenv(configDirEnv) - } - - if len(path) == 0 { //Var is not set - path = configDirectory - } - - return path -} diff --git a/internal/config/environment.go b/internal/config/environment.go deleted file mode 100644 index 97611ac99..000000000 --- a/internal/config/environment.go +++ /dev/null @@ -1,98 +0,0 @@ -/******************************************************************************* - * Copyright 2019 Dell 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 config - -import ( - "net/url" - "os" - "strconv" - "strings" - - "github.com/edgexfoundry/app-functions-sdk-go/internal/common" - "github.com/pelletier/go-toml" -) - -const ( - envKeyRegistryUrl = "edgex_registry" - envKeyServiceUrl = "edgex_service" -) - -// 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]interface{} -} - -// NewEnvironment constructor reads/stores os.Environ() for use by environment receiver methods. -func NewEnvironment() *environment { - osEnv := os.Environ() - e := &environment{ - env: make(map[string]interface{}, 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 -} - -// OverrideRegistryInfoFromEnvironment method overrides registry location with environment variables. -func (e *environment) OverrideRegistryInfoFromEnvironment(registry common.RegistryInfo) common.RegistryInfo { - if env := os.Getenv(envKeyRegistryUrl); env != "" { - if u, err := url.Parse(env); err == nil { - if p, err := strconv.ParseInt(u.Port(), 10, 0); err == nil { - registry.Port = int(p) - registry.Host = u.Hostname() - registry.Type = u.Scheme - } - } - } - return registry -} - -// OverrideServiceInfoFromEnvironment method overrides Service location with environment variables. -func (e *environment) OverrideServiceInfoFromEnvironment(service common.ServiceInfo) common.ServiceInfo { - if env := os.Getenv(envKeyServiceUrl); env != "" { - if u, err := url.Parse(env); err == nil { - if p, err := strconv.ParseInt(u.Port(), 10, 0); err == nil { - service.Port = int(p) - service.Host = u.Hostname() - service.Protocol = u.Scheme - } - } - } - return service -} - -// OverrideRegistryConfigFromEnvironment method replaces values in the toml.Tree for matching environment variable keys. -func (e *environment) OverrideFromEnvironment(tree *toml.Tree) *toml.Tree { - for k, v := range e.env { - k = strings.Replace(k, "_", ".", -1) - switch { - case tree.Has(k): - // global key - tree.Set(k, v) - } - } - return tree -} diff --git a/internal/config/environment_test.go b/internal/config/environment_test.go deleted file mode 100644 index 44230baf4..000000000 --- a/internal/config/environment_test.go +++ /dev/null @@ -1,164 +0,0 @@ -/******************************************************************************* - * Copyright 2019 Dell 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 config - -import ( - "os" - "strconv" - "testing" - - "github.com/edgexfoundry/app-functions-sdk-go/internal/common" - "github.com/pelletier/go-toml" - "github.com/stretchr/testify/assert" -) - -const ( - envValue = "envValue" - rootKey = "rootKey" - rootValue = "rootValue" - sub = "sub" - subKey = "subKey" - subValue = "subValue" - - testToml = ` -` + rootKey + `="` + rootValue + `" -[` + sub + `] -` + subKey + `="` + subValue + `"` -) - -func newSUT(t *testing.T, env map[string]string) *environment { - os.Clearenv() - for k, v := range env { - if err := os.Setenv(k, v); err != nil { - t.Fail() - } - } - return NewEnvironment() -} - -func newOverrideFromEnvironmentSUT(t *testing.T, envKey string, envValue string) (*toml.Tree, *environment) { - tree, err := toml.Load(testToml) - if err != nil { - t.Fail() - } - return tree, newSUT(t, map[string]string{envKey: envValue}) -} - -func TestKeyMatchOverwritesValue(t *testing.T) { - var tests = []struct { - name string - key string - envKey string - envValue string - expectedValue string - }{ - {"generic root", rootKey, rootKey, envValue, envValue}, - {"generic sub", sub + "." + subKey, sub + "." + subKey, envValue, envValue}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - tree, sut := newOverrideFromEnvironmentSUT(t, test.key, test.envValue) - - result := sut.OverrideFromEnvironment(tree) - - assert.Equal(t, test.envValue, result.Get(test.key)) - }) - } -} - -func TestNonMatchingKeyDoesNotOverwritesValue(t *testing.T) { - var tests = []struct { - name string - key string - envKey string - envValue string - expectedValue string - }{ - {"root", rootKey, rootKey, envValue, rootValue}, - {"sub", sub + "." + subKey, sub + "." + subKey, envValue, rootValue}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - tree, sut := newOverrideFromEnvironmentSUT(t, test.key, test.envValue) - - result := sut.OverrideFromEnvironment(tree) - - assert.Equal(t, test.envValue, result.Get(test.key)) - }) - } -} - -const ( - expectedRegistryTypeValue = "consul" - expectedRegistryHostValue = "localhost" - expectedRegistryPortValue = 8500 - - expectedServiceProtocolValue = "http" - expectedServiceHostValue = "localhost" - expectedServicePortValue = 8500 - - defaultHostValue = "defaultHost" - defaultPortValue = 987654321 - defaultTypeValue = "defaultType" -) - -func initializeTest(t *testing.T) common.RegistryInfo { - os.Clearenv() - return common.RegistryInfo{ - Host: defaultHostValue, - Port: defaultPortValue, - Type: defaultTypeValue, - } -} - -func TestEnvVariableUpdatesRegistryInfo(t *testing.T) { - registryInfo := initializeTest(t) - sut := newSUT(t, map[string]string{envKeyRegistryUrl: expectedRegistryTypeValue + "://" + expectedRegistryHostValue + ":" + strconv.Itoa(expectedRegistryPortValue)}) - - registryInfo = sut.OverrideRegistryInfoFromEnvironment(registryInfo) - - assert.Equal(t, expectedRegistryHostValue, registryInfo.Host) - assert.Equal(t, expectedRegistryPortValue, registryInfo.Port) - assert.Equal(t, expectedRegistryTypeValue, registryInfo.Type) -} - -func TestEnvVariableUpdatesServiceInfo(t *testing.T) { - os.Clearenv() - serviceInfo := common.ServiceInfo{ - Host: defaultHostValue, - Port: defaultPortValue, - Protocol: defaultTypeValue, - } - sut := newSUT(t, map[string]string{envKeyServiceUrl: expectedServiceProtocolValue + "://" + expectedServiceHostValue + ":" + strconv.Itoa(expectedServicePortValue)}) - - serviceInfo = sut.OverrideServiceInfoFromEnvironment(serviceInfo) - - assert.Equal(t, expectedServiceHostValue, serviceInfo.Host) - assert.Equal(t, expectedServicePortValue, serviceInfo.Port) - assert.Equal(t, expectedServiceProtocolValue, serviceInfo.Protocol) -} - -func TestNoEnvVariableDoesNotUpdateRegistryInfo(t *testing.T) { - registryInfo := initializeTest(t) - sut := newSUT(t, map[string]string{}) - - registryInfo = sut.OverrideRegistryInfoFromEnvironment(registryInfo) - - assert.Equal(t, defaultHostValue, registryInfo.Host) - assert.Equal(t, defaultPortValue, registryInfo.Port) - assert.Equal(t, defaultTypeValue, registryInfo.Type) -} diff --git a/internal/constants.go b/internal/constants.go index 6236415ba..ff2a873b6 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,13 +23,13 @@ import ( ) const ( - BootTimeoutDefault = time.Duration(30 * time.Second) - ClientMonitorDefault = time.Duration(15 * time.Second) - ConfigFileName = "configuration.toml" - ConfigRegistryStem = "edgex/appservices/1.0/" - WritableKey = "/Writable" - ApiTriggerRoute = "/api/v1/trigger" - DatabaseName = "application-service" + ClientMonitorDefault = 15 * time.Second + BootTimeoutSecondsDefault = 30 + BootRetrySecondsDefault = 1 + ConfigFileName = "configuration.toml" + ConfigRegistryStem = "edgex/appservices/1.0/" + ApiTriggerRoute = "/api/v1/trigger" + DatabaseName = "application-service" ) // SDKVersion indicates the version of the SDK - will be overwritten by build diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 6b32f34fb..51265ee06 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import ( "net/http" "testing" + "github.com/edgexfoundry/go-mod-bootstrap/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ugorji/go/codec" @@ -203,7 +204,7 @@ func TestProcessMessageTransformError(t *testing.T) { expectedErrorCode := http.StatusUnprocessableEntity // Send a RegistryInfo to the pipeline, instead of an Event - registryInfo := common.RegistryInfo{ + registryInfo := config.RegistryInfo{ Host: devID1, } payload, _ := json.Marshal(registryInfo) @@ -216,7 +217,7 @@ func TestProcessMessageTransformError(t *testing.T) { LoggingClient: lc, } // Let the Runtime know we are sending a RegistryInfo so it passes it to the first function - runtime := GolangRuntime{TargetType: &common.RegistryInfo{}} + runtime := GolangRuntime{TargetType: &config.RegistryInfo{}} runtime.Initialize(nil, nil) // FilterByDeviceName with return an error if it doesn't receive and Event runtime.SetTransforms([]appcontext.AppFunction{transforms.NewFilter([]string{"SomeDevice"}).FilterByDeviceName}) @@ -416,7 +417,7 @@ func TestExecutePipelinePersist(t *testing.T) { } ctx := appcontext.Context{ - Configuration: config, + Configuration: &config, LoggingClient: lc, CorrelationID: "CorrelationID", EventChecksum: "EventChecksum", diff --git a/internal/runtime/storeforward.go b/internal/runtime/storeforward.go index 7c267397b..4459e00fd 100644 --- a/internal/runtime/storeforward.go +++ b/internal/runtime/storeforward.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -226,7 +226,7 @@ func (sf *storeForwardInfo) retryExportFunction(item contracts.StoredObject, con CorrelationID: item.CorrelationID, EventChecksum: item.EventChecksum, EventID: item.EventID, - Configuration: *config, + Configuration: config, LoggingClient: edgeXClients.LoggingClient, EventClient: edgeXClients.EventClient, ValueDescriptorClient: edgeXClients.ValueDescriptorClient, diff --git a/internal/security/client/vaultclient.go b/internal/security/client/vaultclient.go index 643562f15..ef23ffa9b 100644 --- a/internal/security/client/vaultclient.go +++ b/internal/security/client/vaultclient.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 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 @@ -20,10 +20,8 @@ import ( "context" "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/internal/common" - + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" - "github.com/edgexfoundry/go-mod-secrets/pkg" "github.com/edgexfoundry/go-mod-secrets/pkg/providers/vault" "github.com/edgexfoundry/go-mod-secrets/pkg/token/authtokenloader" @@ -47,7 +45,7 @@ func NewVault(ctx context.Context, config vault.SecretConfig, lc logger.LoggingC } // Get is the getter for Vault secret client from go-mod-secrets -func (c Vault) Get(secretStoreInfo common.SecretStoreInfo) (pkg.SecretClient, error) { +func (c Vault) Get(secretStoreInfo bootstrapConfig.SecretStoreInfo) (pkg.SecretClient, error) { return vault.NewSecretClientFactory().NewSecretClient( c.ctx, c.config, @@ -58,7 +56,7 @@ func (c Vault) Get(secretStoreInfo common.SecretStoreInfo) (pkg.SecretClient, er // getDefaultTokenExpiredCallback is the default implementation of tokenExpiredCallback function // It utilizes the tokenFile to re-read the token and enable retry if any update from the expired token func (c Vault) getDefaultTokenExpiredCallback( - secretStoreInfo common.SecretStoreInfo) func(expiredToken string) (replacementToken string, retry bool) { + secretStoreInfo bootstrapConfig.SecretStoreInfo) func(expiredToken string) (replacementToken string, retry bool) { // if there is no tokenFile, then no replacement token can be used and hence no callback if secretStoreInfo.TokenFile == "" { return nil diff --git a/internal/security/client/vaultclient_test.go b/internal/security/client/vaultclient_test.go index 3c007209c..36efc88ae 100644 --- a/internal/security/client/vaultclient_test.go +++ b/internal/security/client/vaultclient_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 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 @@ -25,7 +25,8 @@ import ( "testing" "time" - "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/app-functions-sdk-go/internal/security/mock" "github.com/stretchr/testify/assert" @@ -53,14 +54,12 @@ func TestGetVaultClient(t *testing.T) { bkgCtx := context.Background() lc := logger.NewClient("app_functions_sdk_go", false, "./test.log", "DEBUG") - testSecretStoreInfo := common.SecretStoreInfo{ - SecretConfig: vault.SecretConfig{ - Host: host, - Port: portNum, - Path: "/test", - Protocol: "http", - ServerName: "mockVaultServer", - }, + testSecretStoreInfo := config.SecretStoreInfo{ + Host: host, + Port: portNum, + Path: "/test", + Protocol: "http", + ServerName: "mockVaultServer", } tests := []struct { diff --git a/internal/security/secret.go b/internal/security/secret.go index d78d2ac62..e985345fe 100644 --- a/internal/security/secret.go +++ b/internal/security/secret.go @@ -1,5 +1,6 @@ /******************************************************************************** * Copyright 2019 Dell Inc. + * Copyright 2020 Dell 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 @@ -21,6 +22,8 @@ import ( "sync" "time" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/security/authtokenloader" "github.com/edgexfoundry/app-functions-sdk-go/internal/security/client" @@ -78,7 +81,7 @@ func (s *SecretProvider) Initialize(ctx context.Context) bool { func (s *SecretProvider) initializeSecretClient( ctx context.Context, - secretStoreInfo common.SecretStoreInfo) (pkg.SecretClient, error) { + secretStoreInfo bootstrapConfig.SecretStoreInfo) (pkg.SecretClient, error) { var secretClient pkg.SecretClient // secretStoreInfo is optional so that secret config can be empty @@ -127,8 +130,8 @@ func (s *SecretProvider) initializeSecretClient( // getSecretConfig creates a SecretConfig based on the SecretStoreInfo configuration properties. // If a tokenfile is present it will override the Authentication.AuthToken value. // the return boolean is used to indicate whether the secret store configuration is empty or not -func (s *SecretProvider) getSecretConfig(secretStoreInfo common.SecretStoreInfo) (vault.SecretConfig, bool, error) { - emptySecretStore := common.SecretStoreInfo{} +func (s *SecretProvider) getSecretConfig(secretStoreInfo bootstrapConfig.SecretStoreInfo) (vault.SecretConfig, bool, error) { + emptySecretStore := bootstrapConfig.SecretStoreInfo{} if secretStoreInfo == emptySecretStore { return vault.SecretConfig{}, true, nil } diff --git a/internal/security/secret_test.go b/internal/security/secret_test.go index 90abe6ebc..e31052943 100644 --- a/internal/security/secret_test.go +++ b/internal/security/secret_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 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 @@ -26,6 +26,8 @@ import ( "testing" "time" + "github.com/edgexfoundry/go-mod-bootstrap/config" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/security/mock" @@ -64,25 +66,23 @@ func TestInitializeClientFromSecretProvider(t *testing.T) { lc := logger.NewClient("app_functions_sdk_go", false, "./test.log", "DEBUG") - testSecretStoreInfo := common.SecretStoreInfo{ - SecretConfig: vault.SecretConfig{ - Host: host, - Port: portNum, - Protocol: "http", - ServerName: "mockVaultServer", - AdditionalRetryAttempts: 2, - RetryWaitPeriod: "100ms", - }, + testSecretStoreInfo := config.SecretStoreInfo{ + Host: host, + Port: portNum, + Protocol: "http", + ServerName: "mockVaultServer", + AdditionalRetryAttempts: 2, + RetryWaitPeriod: "100ms", } - emptySecretStoreInfo := common.SecretStoreInfo{} + emptySecretStoreInfo := config.SecretStoreInfo{} tests := []struct { name string tokenFileForShared string tokenFileForExclusive string - sharedSecretStore common.SecretStoreInfo - exclusiveSecretStore common.SecretStoreInfo + sharedSecretStore config.SecretStoreInfo + exclusiveSecretStore config.SecretStoreInfo expectError bool expectSharedSecretClientEmpty bool expectExclusiveSecretClientEmpty bool @@ -221,15 +221,13 @@ func TestConfigAdditonalRetryAttempts(t *testing.T) { os.Setenv("EDGEX_SECURITY_SECRET_STORE", "true") - testSecretStoreInfo := common.SecretStoreInfo{ + testSecretStoreInfo := config.SecretStoreInfo{ // configuration with AdditionalRetryAttempts omitted - SecretConfig: vault.SecretConfig{ - Host: host, - Port: portNum, - Protocol: "http", - ServerName: "mockVaultServer", - }, - TokenFile: "client/testdata/testToken.json", + Host: host, + Port: portNum, + Protocol: "http", + ServerName: "mockVaultServer", + TokenFile: "client/testdata/testToken.json", } config := &common.ConfigurationStruct{ diff --git a/internal/trigger/http/rest.go b/internal/trigger/http/rest.go index 2e655d75a..7cea5b4c4 100644 --- a/internal/trigger/http/rest.go +++ b/internal/trigger/http/rest.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,13 +27,14 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" "github.com/edgexfoundry/app-functions-sdk-go/internal/webserver" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-messaging/pkg/types" ) // Trigger implements Trigger to support Triggers type Trigger struct { - Configuration common.ConfigurationStruct + Configuration *common.ConfigurationStruct Runtime *runtime.GolangRuntime outputData []byte Webserver *webserver.WebServer @@ -41,14 +42,14 @@ type Trigger struct { } // Initialize initializes the Trigger for logging and REST route -func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context) error { +func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context) (bootstrap.Deferred, error) { logger := trigger.EdgeXClients.LoggingClient logger.Info("Initializing HTTP Trigger") trigger.Webserver.SetupTriggerRoute(trigger.requestHandler) logger.Info("HTTP Trigger Initialized") - return nil + return nil, nil } func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Request) { diff --git a/internal/trigger/messagebus/messaging.go b/internal/trigger/messagebus/messaging.go index e31b9e2d4..e9baae2fe 100644 --- a/internal/trigger/messagebus/messaging.go +++ b/internal/trigger/messagebus/messaging.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/appcontext" "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/clients" "github.com/edgexfoundry/go-mod-messaging/messaging" "github.com/edgexfoundry/go-mod-messaging/pkg/types" @@ -31,7 +32,7 @@ import ( // Trigger implements Trigger to support MessageBusData type Trigger struct { - Configuration common.ConfigurationStruct + Configuration *common.ConfigurationStruct Runtime *runtime.GolangRuntime client messaging.MessageClient topics []types.TopicChannel @@ -39,7 +40,7 @@ type Trigger struct { } // Initialize ... -func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context) error { +func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context) (bootstrap.Deferred, error) { var err error logger := trigger.EdgeXClients.LoggingClient @@ -47,14 +48,14 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context trigger.client, err = messaging.NewMessageClient(trigger.Configuration.MessageBus) if err != nil { - return err + return nil, err } trigger.topics = []types.TopicChannel{{Topic: trigger.Configuration.Binding.SubscribeTopic, Messages: make(chan types.MessageEnvelope)}} messageErrors := make(chan error) err = trigger.client.Connect() if err != nil { - return err + return nil, err } trigger.client.Subscribe(trigger.topics, messageErrors) @@ -111,5 +112,12 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context } }() - return nil + deferred := func() { + logger.Info("Disconnecting from the message bus") + err := trigger.client.Disconnect() + if err != nil { + logger.Error("Unable to disconnect from the message bus", "error", err.Error()) + } + } + return deferred, nil } diff --git a/internal/trigger/messagebus/messaging_test.go b/internal/trigger/messagebus/messaging_test.go index 546130540..271048a2d 100644 --- a/internal/trigger/messagebus/messaging_test.go +++ b/internal/trigger/messagebus/messaging_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ func TestInitialize(t *testing.T) { runtime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} + trigger := Trigger{Configuration: &config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} trigger.Initialize(&sync.WaitGroup{}, context.Background()) assert.NotNil(t, trigger.client, "Expected client to be set") assert.Equal(t, 1, len(trigger.topics)) @@ -100,8 +100,8 @@ func TestInitializeBadConfiguration(t *testing.T) { runtime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} - err := trigger.Initialize(&sync.WaitGroup{}, context.Background()) + trigger := Trigger{Configuration: &config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} + _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background()) assert.Error(t, err) } @@ -145,7 +145,7 @@ func TestInitializeAndProcessEventWithNoOutput(t *testing.T) { runtime := &runtime.GolangRuntime{} runtime.Initialize(nil, nil) runtime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} + trigger := Trigger{Configuration: &config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} trigger.Initialize(&sync.WaitGroup{}, context.Background()) message := types.MessageEnvelope{ @@ -216,7 +216,7 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { runtime := &runtime.GolangRuntime{} runtime.Initialize(nil, nil) runtime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} + trigger := Trigger{Configuration: &config, Runtime: runtime, EdgeXClients: common.EdgeXClients{LoggingClient: logClient}} testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ diff --git a/internal/trigger/trigger.go b/internal/trigger/trigger.go index 4a13d32d7..aa022d068 100644 --- a/internal/trigger/trigger.go +++ b/internal/trigger/trigger.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ package trigger import ( "context" "sync" + + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap" ) // Trigger interface is used to hold event data and allow function to type Trigger interface { // Initialize performs post creation initializations - Initialize(wg *sync.WaitGroup, ctx context.Context) error + Initialize(wg *sync.WaitGroup, ctx context.Context) (bootstrap.Deferred, error) } diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index eb8e66d7c..8ae8e8375 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2020 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -109,7 +109,7 @@ func TestConfigureAndConfigRoute(t *testing.T) { rr := httptest.NewRecorder() webserver.router.ServeHTTP(rr, req) - expected := `{"Writable":{"LogLevel":"","Pipeline":{"ExecutionOrder":"","UseTargetTypeOfByteArray":false,"Functions":null},"StoreAndForward":{"Enabled":false,"RetryInterval":"","MaxRetryCount":0},"InsecureSecrets":null},"Logging":{"EnableRemote":false,"File":""},"Registry":{"Host":"","Port":0,"Type":""},"Service":{"BootTimeout":"","CheckInterval":"","ClientMonitor":"","Host":"","HTTPSCert":"","HTTPSKey":"","Port":0,"Protocol":"","StartupMsg":"","ReadMaxLimit":0,"Timeout":""},"MessageBus":{"PublishHost":{"Host":"","Port":0,"Protocol":""},"SubscribeHost":{"Host":"","Port":0,"Protocol":""},"Type":"","Optional":null},"Binding":{"Type":"","SubscribeTopic":"","PublishTopic":""},"ApplicationSettings":null,"Clients":null,"Database":{"Type":"","Host":"","Port":0,"Timeout":"","Username":"","Password":"","MaxIdle":0,"BatchSize":0},"SecretStore":{"Host":"","Port":0,"Path":"","Protocol":"","Namespace":"","RootCaCertPath":"","ServerName":"","Authentication":{"AuthType":"","AuthToken":""},"AdditionalRetryAttempts":0,"RetryWaitPeriod":"","TokenFile":""}}` + "\n" + expected := `{"Writable":{"LogLevel":"","Pipeline":{"ExecutionOrder":"","UseTargetTypeOfByteArray":false,"Functions":null},"StoreAndForward":{"Enabled":false,"RetryInterval":"","MaxRetryCount":0},"InsecureSecrets":null},"Logging":{"EnableRemote":false,"File":""},"Registry":{"Host":"","Port":0,"Type":""},"Service":{"BootTimeout":"","CheckInterval":"","ClientMonitor":"","Host":"","HTTPSCert":"","HTTPSKey":"","Port":0,"Protocol":"","StartupMsg":"","ReadMaxLimit":0,"Timeout":""},"MessageBus":{"PublishHost":{"Host":"","Port":0,"Protocol":""},"SubscribeHost":{"Host":"","Port":0,"Protocol":""},"Type":"","Optional":null},"Binding":{"Type":"","SubscribeTopic":"","PublishTopic":""},"ApplicationSettings":null,"Clients":null,"Database":{"Type":"","Host":"","Port":0,"Timeout":"","Username":"","Password":"","MaxIdle":0,"BatchSize":0},"SecretStore":{"Host":"","Port":0,"Path":"","Protocol":"","Namespace":"","RootCaCertPath":"","ServerName":"","Authentication":{"AuthType":"","AuthToken":""},"AdditionalRetryAttempts":0,"RetryWaitPeriod":"","TokenFile":""},"SecretStoreExclusive":{"Host":"","Port":0,"Path":"","Protocol":"","Namespace":"","RootCaCertPath":"","ServerName":"","Authentication":{"AuthType":"","AuthToken":""},"AdditionalRetryAttempts":0,"RetryWaitPeriod":"","TokenFile":""},"Startup":{"Duration":0,"Interval":0}}` + "\n" body := rr.Body.String() assert.Equal(t, expected, body)