From 1e84b84aabd43d23c835153bbcbc2b10431918ee Mon Sep 17 00:00:00 2001 From: lenny Date: Mon, 8 Mar 2021 18:03:22 -0700 Subject: [PATCH 1/3] refactor: Rework SDK to use Interfaces and factory methods Added mocks for interfaces for use is custom service unit tests. Refactored to make better use of the dependency injection container (dic) to propigate the common dependecies throught the layers General file clean up for items flag by IDE as non-standard code. closes #573 Signed-off-by: lenny --- Makefile | 22 +- app-service-template/Attribution.txt | 5 +- app-service-template/Makefile | 16 + app-service-template/functions/sample.go | 49 +- app-service-template/functions/sample_test.go | 42 +- app-service-template/main.go | 38 +- app-service-template/main_test.go | 111 ++++ appcontext/context.go | 157 ----- appcontext/context_test.go | 105 ---- appsdk/sdk.go | 535 ------------------ appsdk/triggerfactory.go | 117 ---- go.mod | 1 - .../app}/backgroundpublisher.go | 16 +- .../app}/backgroundpublisher_test.go | 6 +- {appsdk => internal/app}/configupdates.go | 112 ++-- {appsdk => internal/app}/configurable.go | 154 ++--- {appsdk => internal/app}/configurable_test.go | 95 +--- internal/app/service.go | 532 +++++++++++++++++ .../app/service_test.go | 141 +++-- internal/app/triggerfactory.go | 121 ++++ .../app}/triggerfactory_test.go | 63 ++- internal/appfunction/context.go | 220 +++++++ internal/appfunction/context_test.go | 265 +++++++++ internal/bootstrap/container/config.go | 7 +- internal/bootstrap/handlers/clients.go | 29 +- internal/bootstrap/handlers/clients_test.go | 14 +- internal/bootstrap/handlers/storeclient.go | 10 +- internal/bootstrap/handlers/telemetry.go | 4 +- internal/bootstrap/handlers/version.go | 5 +- internal/bootstrap/handlers/version_test.go | 8 +- internal/common/clients.go | 38 -- internal/constants.go | 7 +- internal/controller/rest/controller.go | 18 +- internal/controller/rest/controller_test.go | 38 +- internal/runtime/runtime.go | 75 ++- internal/runtime/runtime_test.go | 191 +++---- internal/runtime/storeforward.go | 116 ++-- internal/runtime/storeforward_test.go | 83 +-- internal/store/db/db.go | 2 +- internal/store/db/redis/store.go | 12 +- internal/store/factory.go | 3 +- internal/telemetry/telemetry.go | 3 +- internal/telemetry/windows_cpu.go | 8 +- internal/trigger/http/rest.go | 84 +-- internal/trigger/http/rest_test.go | 11 +- internal/trigger/messagebus/messaging.go | 98 ++-- internal/trigger/messagebus/messaging_test.go | 214 ++++--- internal/trigger/mqtt/mqtt.go | 109 ++-- internal/webserver/server.go | 34 +- internal/webserver/server_test.go | 35 +- pkg/factory.go | 61 ++ pkg/factory_test.go | 56 ++ pkg/interfaces/backgroundpublisher.go | 25 + pkg/interfaces/context.go | 77 +++ pkg/interfaces/mocks/AppFunctionContext.go | 211 +++++++ pkg/interfaces/mocks/ApplicationService.go | 301 ++++++++++ pkg/interfaces/mocks/BackgroundPublisher.go | 15 + pkg/interfaces/mocks/Trigger.go | 43 ++ pkg/interfaces/mocks/TriggerContextBuilder.go | 31 + .../mocks/TriggerMessageProcessor.go | 29 + pkg/interfaces/service.go | 102 ++++ {appsdk => pkg/interfaces}/trigger.go | 31 +- pkg/secure/mqttfactory.go | 14 +- pkg/secure/mqttfactory_test.go | 49 +- pkg/transforms/batch.go | 26 +- pkg/transforms/batch_test.go | 18 +- pkg/transforms/compression.go | 52 +- pkg/transforms/compression_test.go | 8 +- pkg/transforms/conversion.go | 29 +- pkg/transforms/conversion_test.go | 78 +-- pkg/transforms/coredata.go | 20 +- pkg/transforms/coredata_test.go | 3 +- pkg/transforms/encryption.go | 22 +- pkg/transforms/encryption_test.go | 17 +- pkg/transforms/filter.go | 44 +- pkg/transforms/filter_test.go | 150 ++--- pkg/transforms/http.go | 40 +- pkg/transforms/http_test.go | 52 +- pkg/transforms/jsonlogic.go | 36 +- pkg/transforms/jsonlogic_test.go | 46 +- pkg/transforms/mqttsecret.go | 47 +- pkg/transforms/mqttsecret_broker_test.go | 44 -- pkg/transforms/mqttsecret_test.go | 14 +- .../{outputdata.go => responsedata.go} | 33 +- ...utputdata_test.go => responsedata_test.go} | 50 +- pkg/transforms/tags.go | 18 +- pkg/transforms/tags_test.go | 13 +- pkg/transforms/testmain_test.go | 60 ++ pkg/util/helpers.go | 4 +- pkg/util/helpers_test.go | 6 +- 90 files changed, 3761 insertions(+), 2393 deletions(-) create mode 100644 app-service-template/main_test.go delete mode 100644 appcontext/context.go delete mode 100644 appcontext/context_test.go delete mode 100644 appsdk/sdk.go delete mode 100644 appsdk/triggerfactory.go rename {appsdk => internal/app}/backgroundpublisher.go (74%) rename {appsdk => internal/app}/backgroundpublisher_test.go (96%) rename {appsdk => internal/app}/configupdates.go (55%) rename {appsdk => internal/app}/configurable.go (73%) rename {appsdk => internal/app}/configurable_test.go (88%) create mode 100644 internal/app/service.go rename appsdk/sdk_test.go => internal/app/service_test.go (80%) create mode 100644 internal/app/triggerfactory.go rename {appsdk => internal/app}/triggerfactory_test.go (81%) create mode 100644 internal/appfunction/context.go create mode 100644 internal/appfunction/context_test.go delete mode 100644 internal/common/clients.go create mode 100644 pkg/factory.go create mode 100644 pkg/factory_test.go create mode 100644 pkg/interfaces/backgroundpublisher.go create mode 100644 pkg/interfaces/context.go create mode 100644 pkg/interfaces/mocks/AppFunctionContext.go create mode 100644 pkg/interfaces/mocks/ApplicationService.go create mode 100644 pkg/interfaces/mocks/BackgroundPublisher.go create mode 100644 pkg/interfaces/mocks/Trigger.go create mode 100644 pkg/interfaces/mocks/TriggerContextBuilder.go create mode 100644 pkg/interfaces/mocks/TriggerMessageProcessor.go create mode 100644 pkg/interfaces/service.go rename {appsdk => pkg/interfaces}/trigger.go (64%) delete mode 100644 pkg/transforms/mqttsecret_broker_test.go rename pkg/transforms/{outputdata.go => responsedata.go} (54%) rename pkg/transforms/{outputdata_test.go => responsedata_test.go} (57%) create mode 100644 pkg/transforms/testmain_test.go diff --git a/Makefile b/Makefile index 7cbb04ef8..e6c18613d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2021 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. +# + .PHONY: test GO=CGO_ENABLED=1 GO111MODULE=on go @@ -8,9 +24,11 @@ build: test-template: make -C ./app-service-template test -test: build test-template +test-sdk: $(GO) test ./... -coverprofile=coverage.out ./... $(GO) vet ./... gofmt -l . [ "`gofmt -l .`" = "" ] - ./app-service-template/bin/test-go-mod-tidy.sh \ No newline at end of file + ./app-service-template/bin/test-go-mod-tidy.sh + +test: build test-template test-sdk \ No newline at end of file diff --git a/app-service-template/Attribution.txt b/app-service-template/Attribution.txt index 5c9181338..d0f067243 100644 --- a/app-service-template/Attribution.txt +++ b/app-service-template/Attribution.txt @@ -172,4 +172,7 @@ github.com/mattn/go-isatty (MIT) https://github.com/mattn/go-isatty https://github.com/mattn/go-isatty/blob/master/LICENSE golang.org/x/sys (Unspecified) https://github.com/golang/sys -https://github.com/golang/sys/blob/master/LICENSE \ No newline at end of file +https://github.com/golang/sys/blob/master/LICENSE + +stretchr/objx (MIT) https://github.com/stretchr/objx +https://github.com/stretchr/objx/blob/master/LICENSE \ No newline at end of file diff --git a/app-service-template/Makefile b/app-service-template/Makefile index 475b268b5..7e309a2b5 100644 --- a/app-service-template/Makefile +++ b/app-service-template/Makefile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2021 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. +# + .PHONY: build test clean docker GO=CGO_ENABLED=1 go diff --git a/app-service-template/functions/sample.go b/app-service-template/functions/sample.go index dbe7a3a7f..6e13fd82d 100644 --- a/app-service-template/functions/sample.go +++ b/app-service-template/functions/sample.go @@ -21,7 +21,10 @@ import ( "fmt" "strings" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" ) @@ -39,27 +42,28 @@ type Sample struct { // LogEventDetails is example of processing an Event and passing the original Event to to next function in the pipeline // For more details on the Context API got here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/ -func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("LogEventDetails called") +func (s *Sample) LogEventDetails(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("LogEventDetails called") - if len(params) < 1 { + if data == nil { // Go here for details on Error Handle: https://docs.edgexfoundry.org/1.3/microservices/application/ErrorHandling/ return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } - edgexcontext.LoggingClient.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d", + lc.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d", event.Id, event.DeviceName, len(event.Readings)) for index, reading := range event.Readings { switch strings.ToLower(reading.ValueType) { case strings.ToLower(v2.ValueTypeBinary): - edgexcontext.LoggingClient.Infof( + lc.Infof( "Reading #%d received with ID=%s, Resource=%s, ValueType=%s, MediaType=%s and BinaryValue of size=`%d`", index+1, reading.Id, @@ -68,7 +72,7 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int reading.MediaType, len(reading.BinaryValue)) default: - edgexcontext.LoggingClient.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`", + lc.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`", index+1, reading.Id, reading.ResourceName, @@ -83,14 +87,15 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int } // ConvertEventToXML is example of transforming an Event and passing the transformed data to to next function in the pipeline -func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("ConvertEventToXML called") +func (s *Sample) ConvertEventToXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("ConvertEventToXML called") - if len(params) < 1 { + if data == nil { return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } @@ -103,7 +108,7 @@ func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...i // Example of DEBUG message which by default you don't want to be logged. // To see debug log messages, Set WRITABLE_LOGLEVEL=DEBUG environment variable or // change LogLevel in configuration.toml before running app service. - edgexcontext.LoggingClient.Debug("Event converted to XML: " + xml) + lc.Debug("Event converted to XML: " + xml) // Returning true indicates that the pipeline execution should continue with the next function // using the event passed as input in this case. @@ -111,26 +116,28 @@ func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...i } // OutputXML is an example of processing transformed data -func (s *Sample) OutputXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("OutputXML called") +func (s *Sample) OutputXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("OutputXML called") - if len(params) < 1 { + if data == nil { return false, errors.New("no XML Received") } - xml, ok := params[0].(string) + xml, ok := data.(string) if !ok { return false, errors.New("type received is not an string") } - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Outputting the following XML: %s", xml)) + lc.Debug(fmt.Sprintf("Outputting the following XML: %s", xml)) // This sends the XML as a response. i.e. publish for MessageBus/MQTT triggers as configured or // HTTP response to for the HTTP Trigger - // For more details on the Complete() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete - edgexcontext.Complete([]byte(xml)) + // For more details on the SetResponseData() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete + ctx.SetResponseData([]byte(xml)) + ctx.SetResponseContentType(clients.ContentTypeXML) // Returning false terminates the pipeline execution, so this should be last function specified in the pipeline, - // which is typical in conjunction with usage of .Complete() function. + // which is typical in conjunction with usage of .SetResponseData() function. return false, nil } diff --git a/app-service-template/functions/sample_test.go b/app-service-template/functions/sample_test.go index 3c91250fd..11b309062 100644 --- a/app-service-template/functions/sample_test.go +++ b/app-service-template/functions/sample_test.go @@ -19,12 +19,15 @@ package functions import ( "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" "github.com/stretchr/testify/require" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -32,12 +35,28 @@ import ( // This file contains example of how to unit test pipeline functions // TODO: Change these sample unit tests to test your custom type and function(s) +var appContext interfaces.AppFunctionContext + +func TestMain(m *testing.M) { + // + // This can be changed to a real logger when needing more debug information output to the console + // lc := logger.NewClient("testing", "DEBUG") + // + lc := logger.NewMockClient() + correlationId := uuid.New().String() + + // NewAppFuncContextForTest creates a context with basic dependencies for unit testing with the passed in logger + // If more additional dependencies (such as mock clients) are required, then use + // NewAppFuncContext(correlationID string, dic *di.Container) and pass in an initialized DIC (dependency injection container) + appContext = pkg.NewAppFuncContextForTest(correlationId, lc) +} + func TestSample_LogEventDetails(t *testing.T) { expectedEvent := createTestEvent(t) expectedContinuePipeline := true target := NewSample() - actualContinuePipeline, actualEvent := target.LogEventDetails(createTestAppSdkContext(), expectedEvent) + actualContinuePipeline, actualEvent := target.LogEventDetails(appContext, expectedEvent) assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Equal(t, expectedEvent, actualEvent) @@ -49,7 +68,7 @@ func TestSample_ConvertEventToXML(t *testing.T) { expectedContinuePipeline := true target := NewSample() - actualContinuePipeline, actualXml := target.ConvertEventToXML(createTestAppSdkContext(), event) + actualContinuePipeline, actualXml := target.ConvertEventToXML(appContext, event) assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Equal(t, expectedXml, actualXml) @@ -58,17 +77,17 @@ func TestSample_ConvertEventToXML(t *testing.T) { func TestSample_OutputXML(t *testing.T) { testEvent := createTestEvent(t) - expectedXml, _ := testEvent.ToXML() + xml, _ := testEvent.ToXML() expectedContinuePipeline := false - appContext := createTestAppSdkContext() + expectedContentType := clients.ContentTypeXML target := NewSample() - actualContinuePipeline, result := target.OutputXML(appContext, expectedXml) - actualXml := string(appContext.OutputData) + actualContinuePipeline, result := target.OutputXML(appContext, xml) + actualContentType := appContext.ResponseContentType() assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Nil(t, result) - assert.Equal(t, expectedXml, actualXml) + assert.Equal(t, expectedContentType, actualContentType) } func createTestEvent(t *testing.T) dtos.Event { @@ -87,10 +106,3 @@ func createTestEvent(t *testing.T) dtos.Event { return event } - -func createTestAppSdkContext() *appcontext.Context { - return &appcontext.Context{ - CorrelationID: uuid.New().String(), - LoggingClient: logger.NewMockClient(), - } -} diff --git a/app-service-template/main.go b/app-service-template/main.go index 10e7d2278..e916402f8 100644 --- a/app-service-template/main.go +++ b/app-service-template/main.go @@ -20,7 +20,8 @@ package main import ( "os" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appsdk" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "new-app-service/functions" @@ -34,38 +35,45 @@ func main() { // TODO: See https://docs.edgexfoundry.org/1.3/microservices/application/ApplicationServices/ // for documentation on application services. - edgexSdk := &appsdk.AppFunctionsSDK{ServiceKey: serviceKey} - if err := edgexSdk.Initialize(); err != nil { - edgexSdk.LoggingClient.Errorf("SDK initialization failed: %s", err.Error()) - os.Exit(-1) + code := CreateAndRunService(serviceKey, pkg.NewAppService) + os.Exit(code) +} + +// CreateAndRunService wraps what would normally be in main() so that it can be unit tested +func CreateAndRunService(serviceKey string, newServiceFactory func(string) (interfaces.ApplicationService, bool)) int { + service, ok := newServiceFactory(serviceKey) + if !ok { + return -1 } + lc := service.LoggingClient() + // TODO: Replace with retrieving your custom ApplicationSettings from configuration - deviceNames, err := edgexSdk.GetAppSettingStrings("DeviceNames") + deviceNames, err := service.GetAppSettingStrings("DeviceNames") if err != nil { - edgexSdk.LoggingClient.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error()) - os.Exit(-1) + lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error()) + return -1 } // TODO: Replace below functions with built in and/or your custom functions for your use case. // See https://docs.edgexfoundry.org/1.3/microservices/application/BuiltIn/ for list of built-in functions sample := functions.NewSample() - err = edgexSdk.SetFunctionsPipeline( + err = service.SetFunctionsPipeline( transforms.NewFilterFor(deviceNames).FilterByDeviceName, sample.LogEventDetails, sample.ConvertEventToXML, sample.OutputXML) if err != nil { - edgexSdk.LoggingClient.Errorf("SetFunctionsPipeline returned error: %s", err.Error()) - os.Exit(-1) + lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error()) + return -1 } - if err := edgexSdk.MakeItRun(); err != nil { - edgexSdk.LoggingClient.Errorf("MakeItRun returned error: %s", err.Error()) - os.Exit(-1) + if err := service.MakeItRun(); err != nil { + lc.Errorf("MakeItRun returned error: %s", err.Error()) + return -1 } // TODO: Do any required cleanup here, if needed - os.Exit(0) + return 0 } diff --git a/app-service-template/main_test.go b/app-service-template/main_test.go new file mode 100644 index 000000000..1e8c7fada --- /dev/null +++ b/app-service-template/main_test.go @@ -0,0 +1,111 @@ +// TODO: Change Copyright to your company if open sourcing or remove header +// +// Copyright (c) 2021 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 main + +import ( + "fmt" + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces/mocks" +) + +// This is an example of how to test the code that would typically be in the main() function use mocks +// Not to helpful for a simple main() , but can be if the main() has more complexity that should be unit tested +// TODO: add/update tests for your customized CreateAndRunService or remove for simple main() + +func TestCreateAndRunService_Success(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + mockAppService.On("MakeItRun").Return(nil) + + return mockAppService, true + } + + expected := 0 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_NewService_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + return nil, false + } + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_GetAppSettingStrings_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return(nil, fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_SetFunctionsPipeline_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_MakeItRun_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + mockAppService.On("MakeItRun").Return(fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} diff --git a/appcontext/context.go b/appcontext/context.go deleted file mode 100644 index 627339def..000000000 --- a/appcontext/context.go +++ /dev/null @@ -1,157 +0,0 @@ -// -// 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 appcontext - -import ( - "context" - "fmt" - "time" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" - "github.com/edgexfoundry/go-mod-core-contracts/v2/models" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" - - "github.com/google/uuid" -) - -// AppFunction is a type alias for func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) -type AppFunction = func(edgexcontext *Context, params ...interface{}) (bool, interface{}) - -// Context ... -type Context struct { - // This is the ID used to track the EdgeX event through entire EdgeX framework. - CorrelationID string - // 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 - // LoggingClient is exposed to allow logging following the preferred logging strategy within EdgeX. - LoggingClient logger.LoggingClient - // EventClient exposes Core Data's EventClient API - EventClient coredata.EventClient - // ValueDescriptorClient exposes Core Data's ValueDescriptor API - ValueDescriptorClient coredata.ValueDescriptorClient - // CommandClient exposes Core Commands' Command API - CommandClient command.CommandClient - // NotificationsClient exposes Support Notification's Notifications API - NotificationsClient notifications.NotificationsClient - // RetryData holds the data to be stored for later retry when the pipeline function returns an error - RetryData []byte - // SecretProvider exposes the support for getting and storing secrets - SecretProvider interfaces.SecretProvider - // ResponseContentType is used for holding custom response type for HTTP trigger - ResponseContentType string -} - -// Complete is optional and provides a way to return the specified data. -// In the case of an HTTP Trigger, the data will be returned as the http response. -// In the case of the message bus trigger, the data will be placed on the specified -// message bus publish topic and host in the configuration. -func (appContext *Context) Complete(output []byte) { - appContext.OutputData = output -} - -// SetRetryData sets the RetryData to the specified payload to be stored for later retry -// when the pipeline function returns an error. -func (appContext *Context) SetRetryData(payload []byte) { - appContext.RetryData = payload -} - -// PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in -// CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. -func (appContext *Context) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { - appContext.LoggingClient.Debug("Pushing to CoreData") - if appContext.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 { - return nil, err - } - - // Temporary use V1 Reading until V2 EventClient is available - // TODO: Change to use dtos.Reading - v1Reading := models.Reading{ - Value: string(val), - ValueType: v2.ValueTypeString, - Origin: now, - Device: deviceName, - Name: readingName, - } - - readings := make([]models.Reading, 0, 1) - readings = append(readings, v1Reading) - - // Temporary use V1 Event until V2 EventClient is available - // TODO: Change to use dtos.Event - v1Event := &models.Event{ - Device: deviceName, - Origin: now, - Readings: readings, - } - - correlation := uuid.New().String() - ctx := context.WithValue(context.Background(), clients.CorrelationHeader, correlation) - result, err := appContext.EventClient.Add(ctx, v1Event) // TODO: Update to use V2 EventClient - if err != nil { - return nil, err - } - v1Event.ID = result - - // TODO: Remove once V2 EventClient is available - v2Reading := dtos.BaseReading{ - Versionable: commonDTO.NewVersionable(), - Id: v1Reading.Id, - Created: v1Reading.Created, - Origin: v1Reading.Origin, - DeviceName: v1Reading.Device, - ResourceName: v1Reading.Name, - ProfileName: "", - ValueType: v1Reading.ValueType, - SimpleReading: dtos.SimpleReading{Value: v1Reading.Value}, - } - - // TODO: Remove once V2 EventClient is available - v2Event := dtos.Event{ - Versionable: commonDTO.NewVersionable(), - Id: result, - DeviceName: v1Event.Device, - Origin: v1Event.Origin, - Readings: []dtos.BaseReading{v2Reading}, - } - return &v2Event, nil -} - -// GetSecrets retrieves secrets from a secret store. -// path specifies the type or location of the secrets to retrieve. -// 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 (appContext *Context) GetSecrets(path string, keys ...string) (map[string]string, error) { - return appContext.SecretProvider.GetSecrets(path, keys...) -} diff --git a/appcontext/context_test.go b/appcontext/context_test.go deleted file mode 100644 index 0ed00e65a..000000000 --- a/appcontext/context_test.go +++ /dev/null @@ -1,105 +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 appcontext - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - localURL "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestComplete(t *testing.T) { - ctx := Context{} - testData := "output data" - ctx.Complete([]byte(testData)) - assert.Equal(t, []byte(testData), ctx.OutputData) -} - -var eventClient coredata.EventClient -var lc logger.LoggingClient - -func init() { - eventClient = coredata.NewEventClient(localURL.New("http://test" + clients.ApiEventRoute)) - lc = logger.NewMockClient() -} - -func TestPushToCore(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("newId")) - if r.Method != http.MethodPost { - t.Errorf("expected http method is POST, active http method is : %s", r.Method) - } - url := clients.ApiEventRoute - if r.URL.EscapedPath() != url { - t.Errorf("expected uri path is %s, actual uri path is %s", url, r.URL.EscapedPath()) - } - - })) - - defer ts.Close() - eventClient = coredata.NewEventClient(localURL.New(ts.URL + clients.ApiEventRoute)) - ctx := Context{ - EventClient: eventClient, - LoggingClient: lc, - } - expectedEvent := &dtos.Event{ - Versionable: common.NewVersionable(), - DeviceName: "device-name", - Readings: []dtos.BaseReading{ - { - Versionable: common.NewVersionable(), - DeviceName: "device-name", - ResourceName: "device-resource", - ValueType: v2.ValueTypeString, - SimpleReading: dtos.SimpleReading{ - Value: "value", - }, - }, - }, - } - actualEvent, err := ctx.PushToCoreData("device-name", "device-resource", "value") - require.NoError(t, err) - - assert.NotNil(t, actualEvent) - assert.Equal(t, expectedEvent.ApiVersion, actualEvent.ApiVersion) - assert.Equal(t, expectedEvent.DeviceName, actualEvent.DeviceName) - assert.True(t, len(expectedEvent.Readings) == 1) - assert.Equal(t, expectedEvent.Readings[0].DeviceName, actualEvent.Readings[0].DeviceName) - assert.Equal(t, expectedEvent.Readings[0].ResourceName, actualEvent.Readings[0].ResourceName) - assert.Equal(t, expectedEvent.Readings[0].Value, actualEvent.Readings[0].Value) - assert.Equal(t, expectedEvent.Readings[0].ValueType, actualEvent.Readings[0].ValueType) - assert.Equal(t, expectedEvent.Readings[0].ApiVersion, actualEvent.Readings[0].ApiVersion) -} - -func TestSetRetryData(t *testing.T) { - ctx := Context{} - testData := "output data" - ctx.SetRetryData([]byte(testData)) - assert.Equal(t, []byte(testData), ctx.RetryData) -} diff --git a/appsdk/sdk.go b/appsdk/sdk.go deleted file mode 100644 index 7db7e4172..000000000 --- a/appsdk/sdk.go +++ /dev/null @@ -1,535 +0,0 @@ -// -// 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" - "errors" - "fmt" - nethttp "net/http" - "os" - "os/signal" - "reflect" - "strings" - "sync" - "syscall" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" - bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" - bootstrapHandlers "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/handlers" - bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" - "github.com/edgexfoundry/go-mod-bootstrap/v2/di" - "github.com/edgexfoundry/go-mod-messaging/v2/messaging" - "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/edgexfoundry/go-mod-registry/v2/registry" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/models" - - "github.com/gorilla/mux" -) - -const ( - // ProfileSuffixPlaceholder is used to create unique names for profiles - ProfileSuffixPlaceholder = "" - envProfile = "EDGEX_PROFILE" - envServiceKey = "EDGEX_SERVICE_KEY" - - TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" - TriggerTypeMQTT = "EXTERNAL-MQTT" - TriggerTypeHTTP = "HTTP" - - OptionalPasswordKey = "Password" -) - -// The key type is unexported to prevent collisions with context keys defined in -// other packages. -type key int - -// SDKKey is the context key for getting the sdk context. Its value of zero is -// arbitrary. If this package defined other context keys, they would have -// different integer values. -const SDKKey key = 0 - -// AppFunctionsSDK provides the necessary struct to create an instance of the Application Functions SDK. Be sure and provide a ServiceKey -// when creating an instance of the SDK. After creating an instance, you'll first want to call .Initialize(), to start up the SDK. Secondly, -// provide the desired transforms for your pipeline by calling .SetFunctionsPipeline(). Lastly, call .MakeItRun() to start listening for events based on -// your configured trigger. -type AppFunctionsSDK struct { - // ServiceKey is the application services' key used for Configuration and Registration when the Registry is enabled - ServiceKey string - // LoggingClient is the EdgeX logger client used to log messages - LoggingClient logger.LoggingClient - // TargetType is the expected type of the incoming data. Must be set to a pointer to an instance of the type. - // Defaults to &models.Event{} if nil. The income data is un-marshaled (JSON or CBOR) in to the type, - // except when &[]byte{} is specified. In this case the []byte data is pass to the first function in the Pipeline. - TargetType interface{} - // EdgexClients allows access to the EdgeX clients such as the CommandClient. - // Note that the individual clients (e.g EdgexClients.CommandClient) may be nil if the Service (Command) is not configured - // in the [Clients] section of the App Service's configuration. - // It is highly recommend that the clients are verified to not be nil before use. - EdgexClients common.EdgeXClients - // RegistryClient is the client used by service to communicate with service registry. - RegistryClient registry.Client - transforms []appcontext.AppFunction - skipVersionCheck bool - usingConfigurablePipeline bool - httpErrors chan error - runtime *runtime.GolangRuntime - webserver *webserver.WebServer - config *common.ConfigurationStruct - storeClient interfaces.StoreClient - secretProvider bootstrapInterfaces.SecretProvider - storeForwardWg *sync.WaitGroup - storeForwardCancelCtx context.CancelFunc - appWg *sync.WaitGroup - appCtx context.Context - appCancelCtx context.CancelFunc - deferredFunctions []bootstrap.Deferred - serviceKeyOverride string - backgroundChannel <-chan types.MessageEnvelope - customTriggerFactories map[string]func(sdk *AppFunctionsSDK) (Trigger, error) - stop context.CancelFunc -} - -// AddRoute allows you to leverage the existing webserver to add routes. -func (sdk *AppFunctionsSDK) AddRoute(route string, handler func(nethttp.ResponseWriter, *nethttp.Request), methods ...string) error { - if route == clients.ApiPingRoute || - route == clients.ApiConfigRoute || - route == clients.ApiMetricsRoute || - route == clients.ApiVersionRoute || - route == internal.ApiTriggerRoute { - return errors.New("route is reserved") - } - return sdk.webserver.AddRoute(route, sdk.addContext(handler), methods...) -} - -// AddBackgroundPublisher will create a channel of provided capacity to be -// consumed by the MessageBus output and return a publisher that writes to it -func (sdk *AppFunctionsSDK) AddBackgroundPublisher(capacity int) BackgroundPublisher { - bgchan, pub := newBackgroundPublisher(capacity) - sdk.backgroundChannel = bgchan - return pub -} - -// MakeItStop will force the service loop to exit in the same fashion as SIGINT/SIGTERM received from the OS -func (sdk *AppFunctionsSDK) MakeItStop() { - if sdk.stop != nil { - sdk.stop() - } else { - sdk.LoggingClient.Warn("MakeItStop called but no stop handler set on SDK - is the service running?") - } -} - -// MakeItRun will initialize and start the trigger as specified in the -// configuration. It will also configure the webserver and start listening on -// the specified port. -func (sdk *AppFunctionsSDK) MakeItRun() error { - runCtx, stop := context.WithCancel(context.Background()) - - sdk.stop = stop - - httpErrors := make(chan error) - defer close(httpErrors) - - sdk.runtime = &runtime.GolangRuntime{ - TargetType: sdk.TargetType, - ServiceKey: sdk.ServiceKey, - } - - sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) - sdk.runtime.SetTransforms(sdk.transforms) - - // determine input type and create trigger for it - t := sdk.setupTrigger(sdk.config, sdk.runtime) - if t == nil { - return errors.New("Failed to create Trigger") - } - - // Initialize the trigger (i.e. start a web server, or connect to message bus) - deferred, err := t.Initialize(sdk.appWg, sdk.appCtx, sdk.backgroundChannel) - if err != nil { - sdk.LoggingClient.Error(err.Error()) - return errors.New("Failed to initialize Trigger") - } - - // 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 { - sdk.LoggingClient.Info("StoreAndForward disabled. Not running retry loop.") - } - - sdk.LoggingClient.Info(sdk.config.Service.StartupMsg) - - signals := make(chan os.Signal) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - - sdk.webserver.StartWebServer(sdk.httpErrors) - - select { - case httpError := <-sdk.httpErrors: - sdk.LoggingClient.Info("Http error received: ", httpError.Error()) - err = httpError - - case signalReceived := <-signals: - sdk.LoggingClient.Info("Terminating signal received: " + signalReceived.String()) - - case <-runCtx.Done(): - sdk.LoggingClient.Info("Terminating: sdk.MakeItStop called") - } - - sdk.stop = nil - - if sdk.config.Writable.StoreAndForward.Enabled { - sdk.storeForwardCancelCtx() - sdk.storeForwardWg.Wait() - } - - 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 -} - -// LoadConfigurablePipeline ... -func (sdk *AppFunctionsSDK) LoadConfigurablePipeline() ([]appcontext.AppFunction, error) { - var pipeline []appcontext.AppFunction - - sdk.usingConfigurablePipeline = true - - sdk.TargetType = nil - - if sdk.config.Writable.Pipeline.UseTargetTypeOfByteArray { - sdk.TargetType = &[]byte{} - } - - configurable := AppFunctionsSDKConfigurable{ - Sdk: sdk, - } - valueOfType := reflect.ValueOf(configurable) - pipelineConfig := sdk.config.Writable.Pipeline - executionOrder := util.DeleteEmptyAndTrim(strings.FieldsFunc(pipelineConfig.ExecutionOrder, util.SplitComma)) - - if len(executionOrder) <= 0 { - return nil, errors.New( - "execution Order has 0 functions specified. You must have a least one function in the pipeline") - } - sdk.LoggingClient.Debugf("Function Pipeline Execution Order: [%s]", pipelineConfig.ExecutionOrder) - - for _, functionName := range executionOrder { - functionName = strings.TrimSpace(functionName) - configuration, ok := pipelineConfig.Functions[functionName] - if !ok { - return nil, fmt.Errorf("function '%s' configuration not found in Pipeline.Functions section", functionName) - } - - result := valueOfType.MethodByName(functionName) - if result.Kind() == reflect.Invalid { - return nil, fmt.Errorf("function %s is not a built in SDK function", functionName) - } else if result.IsNil() { - return nil, fmt.Errorf("invalid/missing configuration for %s", functionName) - } - - // determine number of parameters required for function call - inputParameters := make([]reflect.Value, result.Type().NumIn()) - // set keys to be all lowercase to avoid casing issues from configuration - for key := range configuration.Parameters { - value := configuration.Parameters[key] - delete(configuration.Parameters, key) // Make sure the old key has been removed so don't have multiples - configuration.Parameters[strings.ToLower(key)] = value - } - for index := range inputParameters { - parameter := result.Type().In(index) - - switch parameter { - case reflect.TypeOf(map[string]string{}): - inputParameters[index] = reflect.ValueOf(configuration.Parameters) - - default: - return nil, fmt.Errorf( - "function %s has an unsupported parameter type: %s", - functionName, - parameter.String(), - ) - } - } - - function, ok := result.Call(inputParameters)[0].Interface().(appcontext.AppFunction) - if !ok { - return nil, fmt.Errorf("failed to cast function %s as AppFunction type", functionName) - } - - if function == nil { - return nil, fmt.Errorf("%s from configuration failed", functionName) - } - - pipeline = append(pipeline, function) - configurable.Sdk.LoggingClient.Debugf( - "%s function added to configurable pipeline with parameters: [%s]", - functionName, - listParameters(configuration.Parameters)) - } - - return pipeline, nil -} - -func listParameters(parameters map[string]string) string { - result := "" - first := true - for key, value := range parameters { - if first { - result = fmt.Sprintf("%s='%s'", key, value) - first = false - continue - } - - result += fmt.Sprintf(", %s='%s'", key, value) - } - - return result -} - -// SetFunctionsPipeline allows you to define each function to execute and the order in which each function -// will be called as each event comes in. -func (sdk *AppFunctionsSDK) SetFunctionsPipeline(transforms ...appcontext.AppFunction) error { - if len(transforms) == 0 { - return errors.New("no transforms provided to pipeline") - } - - sdk.transforms = transforms - - if sdk.runtime != nil { - sdk.runtime.SetTransforms(transforms) - sdk.runtime.TargetType = sdk.TargetType - } - - return nil -} - -// ApplicationSettings returns the values specified in the custom configuration section. -func (sdk *AppFunctionsSDK) ApplicationSettings() map[string]string { - return sdk.config.ApplicationSettings -} - -// GetAppSettingStrings returns the strings slice for the specified App Setting. -func (sdk *AppFunctionsSDK) GetAppSettingStrings(setting string) ([]string, error) { - if sdk.config.ApplicationSettings == nil { - return nil, fmt.Errorf("%s setting not found: ApplicationSettings section is missing", setting) - } - - settingValue, ok := sdk.config.ApplicationSettings[setting] - if !ok { - return nil, fmt.Errorf("%s setting not found in ApplicationSettings", setting) - } - - valueStrings := util.DeleteEmptyAndTrim(strings.FieldsFunc(settingValue, util.SplitComma)) - - return valueStrings, nil -} - -// Initialize will parse command line flags, register for interrupts, -// initialize the logging system, and ingest configuration. -func (sdk *AppFunctionsSDK) Initialize() error { - startupTimer := startup.NewStartUpTimer(sdk.ServiceKey) - - additionalUsage := - " -s/--skipVersionCheck Indicates the service should skip the Core Service's version compatibility check.\n" + - " -sk/--serviceKey Overrides the service service key used with Registry and/or Configuration Providers.\n" + - " If the name provided contains the text ``, this text will be replaced with\n" + - " the name of the profile used." - - sdkFlags := flags.NewWithUsage(additionalUsage) - sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "skipVersionCheck", false, "") - sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "s", false, "") - sdkFlags.FlagSet.StringVar(&sdk.serviceKeyOverride, "serviceKey", "", "") - sdkFlags.FlagSet.StringVar(&sdk.serviceKeyOverride, "sk", "", "") - - sdkFlags.Parse(os.Args[1:]) - - // Temporarily setup logging to STDOUT so the client can be used before bootstrapping is completed - sdk.LoggingClient = logger.NewClient(sdk.ServiceKey, models.InfoLog) - - sdk.setServiceKey(sdkFlags.Profile()) - - sdk.LoggingClient.Info(fmt.Sprintf("Starting %s %s ", sdk.ServiceKey, internal.ApplicationVersion)) - - 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{} - - 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{ - bootstrapHandlers.SecureProviderBootstrapHandler, - 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 = bootstrapContainer.SecretProviderFrom(dic.Get) - sdk.storeClient = container.StoreClientFrom(dic.Get) - sdk.LoggingClient = bootstrapContainer.LoggingClientFrom(dic.Get) - sdk.RegistryClient = bootstrapContainer.RegistryFrom(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) - - // If using the RedisStreams MessageBus implementation then need to make sure the - // password for the Redis DB is set in the MessageBus Optional properties. - triggerType := strings.ToUpper(sdk.config.Trigger.Type) - if triggerType == TriggerTypeMessageBus && - sdk.config.Trigger.EdgexMessageBus.Type == messaging.RedisStreams { - - credentials, err := sdk.secretProvider.GetSecrets(sdk.config.Database.Type) - if err != nil { - return fmt.Errorf("unable to set RedisStreams password from DB credentials") - } - sdk.config.Trigger.EdgexMessageBus.Optional[OptionalPasswordKey] = credentials[secret.PasswordKey] - } - - // 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.ConfigureStandardRoutes() - - sdk.LoggingClient.Info("Service started in: " + startupTimer.SinceAsString()) - - return nil -} - -// 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) addContext(next func(nethttp.ResponseWriter, *nethttp.Request)) func(nethttp.ResponseWriter, *nethttp.Request) { - return func(w nethttp.ResponseWriter, r *nethttp.Request) { - ctx := context.WithValue(r.Context(), SDKKey, sdk) - next(w, r.WithContext(ctx)) - } -} - -func (sdk *AppFunctionsSDK) addDeferred(deferred bootstrap.Deferred) { - if deferred != nil { - sdk.deferredFunctions = append(sdk.deferredFunctions, deferred) - } -} - -// setServiceKey creates the service's service key with profile name if the original service key has the -// appropriate profile placeholder, otherwise it leaves the original service key unchanged -func (sdk *AppFunctionsSDK) setServiceKey(profile string) { - envValue := os.Getenv(envServiceKey) - if len(envValue) > 0 { - sdk.serviceKeyOverride = envValue - sdk.LoggingClient.Info( - fmt.Sprintf("Environment profileOverride of '-n/--serviceName' by environment variable: %s=%s", - envServiceKey, - envValue)) - } - - // serviceKeyOverride may have been set by the -n/--serviceName command-line option and not the environment variable - if len(sdk.serviceKeyOverride) > 0 { - sdk.ServiceKey = sdk.serviceKeyOverride - } - - if !strings.Contains(sdk.ServiceKey, ProfileSuffixPlaceholder) { - // No placeholder, so nothing to do here - return - } - - // Have to handle environment override here before common bootstrap is used so it is passed the proper service key - profileOverride := os.Getenv(envProfile) - if len(profileOverride) > 0 { - profile = profileOverride - } - - if len(profile) > 0 { - sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, profile, 1) - return - } - - // No profile specified so remove the placeholder text - sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, "", 1) -} diff --git a/appsdk/triggerfactory.go b/appsdk/triggerfactory.go deleted file mode 100644 index 56b0d83f5..000000000 --- a/appsdk/triggerfactory.go +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright (c) 2020 Technocrats -// -// 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 ( - "errors" - "fmt" - "strings" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" - - "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" -) - -func (sdk *AppFunctionsSDK) defaultTriggerMessageProcessor(edgexcontext *appcontext.Context, envelope types.MessageEnvelope) error { - messageError := sdk.runtime.ProcessMessage(edgexcontext, envelope) - - if messageError != nil { - // ProcessMessage logs the error, so no need to log it here. - return messageError.Err - } - - return nil -} - -func (sdk *AppFunctionsSDK) defaultTriggerContextBuilder(env types.MessageEnvelope) *appcontext.Context { - return &appcontext.Context{ - CorrelationID: env.CorrelationID, - Configuration: sdk.config, - LoggingClient: sdk.LoggingClient, - EventClient: sdk.EdgexClients.EventClient, - ValueDescriptorClient: sdk.EdgexClients.ValueDescriptorClient, - CommandClient: sdk.EdgexClients.CommandClient, - NotificationsClient: sdk.EdgexClients.NotificationsClient, - SecretProvider: sdk.secretProvider, - } -} - -// RegisterCustomTriggerFactory allows users to register builders for custom trigger types -func (sdk *AppFunctionsSDK) RegisterCustomTriggerFactory(name string, - factory func(TriggerConfig) (Trigger, error)) error { - nu := strings.ToUpper(name) - - if nu == TriggerTypeMessageBus || - nu == TriggerTypeHTTP || - nu == TriggerTypeMQTT { - return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) - } - - if sdk.customTriggerFactories == nil { - sdk.customTriggerFactories = make(map[string]func(sdk *AppFunctionsSDK) (Trigger, error), 1) - } - - sdk.customTriggerFactories[nu] = func(sdk *AppFunctionsSDK) (Trigger, error) { - return factory(TriggerConfig{ - Config: sdk.config, - Logger: sdk.LoggingClient, - ContextBuilder: sdk.defaultTriggerContextBuilder, - MessageProcessor: sdk.defaultTriggerMessageProcessor, - }) - } - - return nil -} - -// setupTrigger configures the appropriate trigger as specified by configuration. -func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) Trigger { - var t Trigger - // Need to make dynamic, search for the trigger that is input - - switch triggerType := strings.ToUpper(configuration.Trigger.Type); triggerType { - case TriggerTypeHTTP: - sdk.LoggingClient.Info("HTTP trigger selected") - t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients} - - case TriggerTypeMessageBus: - sdk.LoggingClient.Info("EdgeX MessageBus trigger selected") - t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients} - - case TriggerTypeMQTT: - sdk.LoggingClient.Info("External MQTT trigger selected") - t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider) - - default: - if factory, found := sdk.customTriggerFactories[triggerType]; found { - var err error - t, err = factory(sdk) - if err != nil { - sdk.LoggingClient.Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) - return nil - } - } else { - sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type)) - } - } - - return t -} diff --git a/go.mod b/go.mod index ec9542a01..577e7c314 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,5 @@ require ( github.com/gomodule/redigo v2.0.0+incompatible github.com/google/uuid v1.2.0 github.com/gorilla/mux v1.8.0 - github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.7.0 ) diff --git a/appsdk/backgroundpublisher.go b/internal/app/backgroundpublisher.go similarity index 74% rename from appsdk/backgroundpublisher.go rename to internal/app/backgroundpublisher.go index f2d9728f4..b49f2fa0a 100644 --- a/appsdk/backgroundpublisher.go +++ b/internal/app/backgroundpublisher.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +15,13 @@ // limitations under the License. // -package appsdk +package app -import "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +import ( + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" -// BackgroundPublisher provides an interface to send messages from background processes -// through the service's configured MessageBus output -type BackgroundPublisher interface { - // Publish provided message through the configured MessageBus output - Publish(payload []byte, correlationID string, contentType string) -} + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) type backgroundPublisher struct { output chan<- types.MessageEnvelope @@ -40,7 +38,7 @@ func (pub *backgroundPublisher) Publish(payload []byte, correlationID string, co pub.output <- outputEnvelope } -func newBackgroundPublisher(capacity int) (<-chan types.MessageEnvelope, BackgroundPublisher) { +func newBackgroundPublisher(capacity int) (<-chan types.MessageEnvelope, interfaces.BackgroundPublisher) { backgroundChannel := make(chan types.MessageEnvelope, capacity) return backgroundChannel, &backgroundPublisher{output: backgroundChannel} } diff --git a/appsdk/backgroundpublisher_test.go b/internal/app/backgroundpublisher_test.go similarity index 96% rename from appsdk/backgroundpublisher_test.go rename to internal/app/backgroundpublisher_test.go index 9cc55ce71..5053581f4 100644 --- a/appsdk/backgroundpublisher_test.go +++ b/internal/app/backgroundpublisher_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,12 +15,13 @@ // limitations under the License. // -package appsdk +package app import ( - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestNewBackgroundPublisherAndPublish(t *testing.T) { diff --git a/appsdk/configupdates.go b/internal/app/configupdates.go similarity index 55% rename from appsdk/configupdates.go rename to internal/app/configupdates.go index f47a4050f..e046650d5 100644 --- a/appsdk/configupdates.go +++ b/internal/app/configupdates.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,86 +13,78 @@ // See the License for the specific language governing permissions and // limitations under the License. -package appsdk +package app import ( "context" - "fmt" "sync" "time" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" ) // ConfigUpdateProcessor contains the data need to process configuration updates type ConfigUpdateProcessor struct { - sdk *AppFunctionsSDK + svc *Service } -// NewConfigUpdateProcessor creates a new ConfigUpdateProcessor which process configuration updates triggered from +// NewConfigUpdateProcessor creates a new ConfigUpdateProcessor which processes configuration updates triggered from // the Configuration Provider -func NewConfigUpdateProcessor(sdk *AppFunctionsSDK) *ConfigUpdateProcessor { - return &ConfigUpdateProcessor{sdk: sdk} +func NewConfigUpdateProcessor(svc *Service) *ConfigUpdateProcessor { + return &ConfigUpdateProcessor{svc: svc} } // 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) + svc := processor.svc + svc.ctx.appWg.Add(1) go func() { - defer sdk.appWg.Done() + defer svc.ctx.appWg.Done() + lc := svc.LoggingClient() + lc.Info("Waiting for App Service configuration updates...") - sdk.LoggingClient.Info("Waiting for App Service configuration updates...") - - previousWriteable := sdk.config.Writable + previousWriteable := svc.config.Writable for { select { - case <-sdk.appCtx.Done(): - sdk.LoggingClient.Info("Exiting waiting for App Service configuration updates") + case <-svc.ctx.appCtx.Done(): + lc.Info("Exiting waiting for App Service configuration updates") return case <-configUpdated: - currentWritable := sdk.config.Writable - sdk.LoggingClient.Info("Processing App Service configuration updates") + currentWritable := svc.config.Writable + lc.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.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")) + lc.Warn("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)) + lc.Infof("StoreAndForward MaxRetryCount changed to %d", currentWritable.StoreAndForward.MaxRetryCount) case previousWriteable.StoreAndForward.RetryInterval != currentWritable.StoreAndForward.RetryInterval: if _, err := time.ParseDuration(currentWritable.StoreAndForward.RetryInterval); err != nil { - sdk.LoggingClient.Error(fmt.Sprintf("StoreAndForward RetryInterval not change: %s", err.Error())) + lc.Errorf("StoreAndForward RetryInterval not change: %s", err.Error()) currentWritable.StoreAndForward.RetryInterval = previousWriteable.StoreAndForward.RetryInterval continue } processor.processConfigChangedStoreForwardRetryInterval() - sdk.LoggingClient.Info( - fmt.Sprintf( - "StoreAndForward RetryInterval changed to %s", - currentWritable.StoreAndForward.RetryInterval)) + lc.Infof("StoreAndForward RetryInterval changed to %s", currentWritable.StoreAndForward.RetryInterval) case previousWriteable.StoreAndForward.Enabled != currentWritable.StoreAndForward.Enabled: processor.processConfigChangedStoreForwardEnabled() - sdk.LoggingClient.Info( - fmt.Sprintf( - "StoreAndForward Enabled changed to %v", - currentWritable.StoreAndForward.Enabled)) + lc.Infof("StoreAndForward Enabled changed to %v", currentWritable.StoreAndForward.Enabled) default: // Assume change is in the pipeline since all others have been checked appropriately @@ -106,9 +98,8 @@ func (processor *ConfigUpdateProcessor) WaitForConfigUpdates(configUpdated confi }() } -// processConfigChangedStoreForwardRetryInterval handles when the Store and Forward RetryInterval setting has been updated func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardRetryInterval() { - sdk := processor.sdk + sdk := processor.svc if sdk.config.Writable.StoreAndForward.Enabled { sdk.stopStoreForward() @@ -116,23 +107,30 @@ func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardRetryInt } } -// processConfigChangedStoreForwardEnabled handles when the Store and Forward Enabled setting has been updated func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardEnabled() { - sdk := processor.sdk + sdk := processor.svc if sdk.config.Writable.StoreAndForward.Enabled { + storeClient := container.StoreClientFrom(sdk.dic.Get) // StoreClient must be set up for StoreAndForward - if sdk.storeClient == nil { + if storeClient == nil { var err error - startupTimer := startup.NewStartUpTimer(sdk.ServiceKey) - sdk.storeClient, err = handlers.InitializeStoreClient(sdk.secretProvider, sdk.config, startupTimer, sdk.LoggingClient) + startupTimer := startup.NewStartUpTimer(sdk.serviceKey) + secretProvider := bootstrapContainer.SecretProviderFrom(sdk.dic.Get) + storeClient, err = handlers.InitializeStoreClient(secretProvider, sdk.config, startupTimer, sdk.LoggingClient()) if err != nil { // Error already logged sdk.config.Writable.StoreAndForward.Enabled = false return } - sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) + sdk.dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return storeClient + }, + }) + + sdk.runtime.Initialize(sdk.dic) } sdk.startStoreForward() @@ -141,14 +139,13 @@ func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardEnabled( } } -// processConfigChangedPipeline handles when any of the Pipeline settings have been updated func (processor *ConfigUpdateProcessor) processConfigChangedPipeline() { - sdk := processor.sdk + sdk := processor.svc if sdk.usingConfigurablePipeline { transforms, err := sdk.LoadConfigurablePipeline() if err != nil { - sdk.LoggingClient.Error("unable to reload Configurable Pipeline from new configuration: " + err.Error()) + sdk.LoggingClient().Error("unable to reload Configurable Pipeline from new configuration: " + err.Error()) // Reset the transforms so error occurs when attempting to execute the pipeline. sdk.transforms = nil sdk.runtime.SetTransforms(nil) @@ -157,32 +154,23 @@ func (processor *ConfigUpdateProcessor) processConfigChangedPipeline() { err = sdk.SetFunctionsPipeline(transforms...) if err != nil { - sdk.LoggingClient.Error("unable to set Configurable Pipeline functions from new configuration: " + err.Error()) + sdk.LoggingClient().Error("unable to set Configurable Pipeline functions from new configuration: " + err.Error()) return } - sdk.LoggingClient.Info("Configurable Pipeline successfully reloaded from new configuration") + sdk.LoggingClient().Info("Configurable Pipeline successfully reloaded from new configuration") } } -// startStoreForward starts the Store and Forward processing -func (sdk *AppFunctionsSDK) startStoreForward() { +func (svc *Service) 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) + svc.ctx.storeForwardWg = &sync.WaitGroup{} + storeForwardEnabledCtx, svc.ctx.storeForwardCancelCtx = context.WithCancel(context.Background()) + svc.runtime.StartStoreAndForward(svc.ctx.appWg, svc.ctx.appCtx, svc.ctx.storeForwardWg, storeForwardEnabledCtx, svc.serviceKey) } -// 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() +func (svc *Service) stopStoreForward() { + svc.LoggingClient().Info("Canceling Store and Forward retry loop") + svc.ctx.storeForwardCancelCtx() + svc.ctx.storeForwardWg.Wait() } diff --git a/appsdk/configurable.go b/internal/app/configurable.go similarity index 73% rename from appsdk/configurable.go rename to internal/app/configurable.go index a5d69b54d..6b5701c56 100644 --- a/appsdk/configurable.go +++ b/internal/app/configurable.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,18 @@ // limitations under the License. // -package appsdk +package app import ( "fmt" "strconv" "strings" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" ) const ( @@ -81,10 +83,17 @@ type postPutParameters struct { secretName string } -// AppFunctionsSDKConfigurable contains the helper functions that return the function pointers for building the configurable function pipeline. +// Configurable contains the helper functions that return the function pointers for building the configurable function pipeline. // They transform the parameters map from the Pipeline configuration in to the actual actual parameters required by the function. -type AppFunctionsSDKConfigurable struct { - Sdk *AppFunctionsSDK +type Configurable struct { + lc logger.LoggingClient +} + +// NewConfigurable returns a new instance of Configurable +func NewConfigurable(lc logger.LoggingClient) *Configurable { + return &Configurable{ + lc: lc, + } } // FilterByProfileName - Specify the profile names of interest to filter for data coming from certain sensors. @@ -95,8 +104,8 @@ type AppFunctionsSDKConfigurable struct { // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByProfileName", parameters, ProfileNames) +func (app *Configurable) FilterByProfileName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByProfileName", parameters, ProfileNames) if !ok { return nil } @@ -112,8 +121,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[st // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByDeviceName", parameters, DeviceNames) +func (app *Configurable) FilterByDeviceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByDeviceName", parameters, DeviceNames) if !ok { return nil } @@ -129,8 +138,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[str // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterBySourceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterBySourceName", parameters, SourceNames) +func (app *Configurable) FilterBySourceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterBySourceName", parameters, SourceNames) if !ok { return nil } @@ -146,8 +155,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterBySourceName(parameters map[str // event is received or if no data is received. // For example, pressure reading data does not go to functions only interested in motion data. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByResourceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByResourceName", parameters, ResourceNames) +func (app *Configurable) FilterByResourceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByResourceName", parameters, ResourceNames) if !ok { return nil } @@ -158,10 +167,10 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByResourceName(parameters map[s // Transform transforms an EdgeX event to XML or JSON based on specified transform type. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Transform(parameters map[string]string) interfaces.AppFunction { transformType, ok := parameters[TransformType] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Transform", TransformType) + app.lc.Errorf("Could not find '%s' parameter for Transform", TransformType) return nil } @@ -173,7 +182,7 @@ func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]strin case TransformJson: return transform.TransformToJSON default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid transform type '%s'. Must be '%s' or '%s'", transformType, TransformXml, @@ -185,15 +194,15 @@ func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]strin // PushToCore pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in // CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) PushToCore(parameters map[string]string) interfaces.AppFunction { deviceName, ok := parameters[DeviceName] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + DeviceName) + app.lc.Error("Could not find " + DeviceName) return nil } readingName, ok := parameters[ReadingName] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + readingName) + app.lc.Error("Could not find " + readingName) return nil } deviceName = strings.TrimSpace(deviceName) @@ -208,10 +217,10 @@ func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]stri // Compress compresses data received as either a string,[]byte, or json.Marshaller using the specified algorithm (GZIP or ZLIB) // and returns a base64 encoded string as a []byte. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Compress(parameters map[string]string) interfaces.AppFunction { algorithm, ok := parameters[Algorithm] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Compress", Algorithm) + app.lc.Errorf("Could not find '%s' parameter for Compress", Algorithm) return nil } @@ -223,7 +232,7 @@ func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string case CompressZLIB: return transform.CompressWithZLIB default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid compression algorithm '%s'. Must be '%s' or '%s'", algorithm, CompressGZIP, @@ -235,10 +244,10 @@ func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string // Encrypt encrypts either a string, []byte, or json.Marshaller type using specified encryption // algorithm (AES only at this time). It will return a byte[] of the encrypted data. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Encrypt(parameters map[string]string) interfaces.AppFunction { algorithm, ok := parameters[Algorithm] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Encrypt", Algorithm) + app.lc.Errorf("Could not find '%s' parameter for Encrypt", Algorithm) return nil } @@ -251,19 +260,19 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) // If EncryptionKey not specified, then SecretPath & SecretName must be specified if len(encryptionKey) == 0 && (len(secretPath) == 0 || len(secretName) == 0) { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' or '%s' and '%s' in configuration", EncryptionKey, SecretPath, SecretName) + app.lc.Errorf("Could not find '%s' or '%s' and '%s' in configuration", EncryptionKey, SecretPath, SecretName) return nil } // SecretPath & SecretName both must be specified it one of them is. if (len(secretPath) != 0 && len(secretName) == 0) || (len(secretPath) == 0 && len(secretName) != 0) { - dynamic.Sdk.LoggingClient.Errorf("'%s' and '%s' both must be set in configuration", SecretPath, SecretName) + app.lc.Errorf("'%s' and '%s' both must be set in configuration", SecretPath, SecretName) return nil } initVector, ok := parameters[InitVector] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + InitVector) + app.lc.Error("Could not find " + InitVector) return nil } @@ -278,7 +287,7 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) case EncryptAES: return transform.EncryptWithAES default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid encryption algorithm '%s'. Must be '%s'", algorithm, EncryptAES) @@ -290,10 +299,10 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) // then the event that triggered the pipeline will be used. Passing an empty string to the mimetype // method will default to application/json. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]string) appcontext.AppFunction { - params, err := dynamic.processHttpExportParameters(parameters) +func (app *Configurable) HTTPExport(parameters map[string]string) interfaces.AppFunction { + params, err := app.processHttpExportParameters(parameters) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) return nil } @@ -316,7 +325,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]stri case ExportMethodPut: return transform.HTTPPut default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid HTTPExport method of '%s'. Must be '%s' or '%s'", params.method, ExportMethodPost, @@ -329,7 +338,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]stri // MQTTExport will send data from the previous function to the specified Endpoint via MQTT publish. If no previous function exists, // then the event that triggered the pipeline will be used. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) MQTTExport(parameters map[string]string) interfaces.AppFunction { var err error qos := 0 retain := false @@ -338,35 +347,35 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri brokerAddress, ok := parameters[BrokerAddress] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BrokerAddress) + app.lc.Error("Could not find " + BrokerAddress) return nil } topic, ok := parameters[Topic] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + Topic) + app.lc.Error("Could not find " + Topic) return nil } secretPath, ok := parameters[SecretPath] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + SecretPath) + app.lc.Error("Could not find " + SecretPath) return nil } authMode, ok := parameters[AuthMode] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + AuthMode) + app.lc.Error("Could not find " + AuthMode) return nil } clientID, ok := parameters[ClientID] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + ClientID) + app.lc.Error("Could not find " + ClientID) return nil } qosVal, ok := parameters[Qos] if ok { qos, err = strconv.Atoi(qosVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + Qos + " value") + app.lc.Error("Unable to parse " + Qos + " value") return nil } } @@ -374,7 +383,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { retain, err = strconv.ParseBool(retainVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + Retain + " value") + app.lc.Error("Unable to parse " + Retain + " value") return nil } } @@ -382,7 +391,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { autoReconnect, err = strconv.ParseBool(autoreconnectVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + AutoReconnect + " value") + app.lc.Error("Unable to parse " + AutoReconnect + " value") return nil } } @@ -390,7 +399,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { skipCertVerify, err = strconv.ParseBool(skipVerifyVal) if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", skipVerifyVal, SkipVerify), "error", err) + app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", skipVerifyVal, SkipVerify), "error", err) return nil } } @@ -411,7 +420,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { persistOnError, err = strconv.ParseBool(value) if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err) + app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err) return nil } } @@ -419,27 +428,28 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri return transform.MQTTSend } -// SetOutputData sets the output data to that passed in from the previous function. -// It will return an error and stop the pipeline if data passed in is not of type []byte, string or json.Marshaller +// SetResponseData sets the response data to that passed in from the previous function and the response content type +// to that set in the ResponseContentType configuration parameter. It will return an error and stop the pipeline if +// data passed in is not of type []byte, string or json.Marshaller // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) SetOutputData(parameters map[string]string) appcontext.AppFunction { - transform := transforms.OutputData{} +func (app *Configurable) SetResponseData(parameters map[string]string) interfaces.AppFunction { + transform := transforms.ResponseData{} value, ok := parameters[ResponseContentType] if ok && len(value) > 0 { transform.ResponseContentType = value } - return transform.SetOutputData + return transform.SetResponseData } // Batch sets up Batching of events based on the specified mode parameter (BatchByCount, BatchByTime or BatchByTimeAndCount) // and mode specific parameters. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Batch(parameters map[string]string) interfaces.AppFunction { mode, ok := parameters[Mode] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Batch", Mode) + app.lc.Errorf("Could not find '%s' parameter for Batch", Mode) return nil } @@ -447,13 +457,13 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a case BatchByCount: batchThreshold, ok := parameters[BatchThreshold] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByCount", BatchThreshold) + app.lc.Errorf("Could not find '%s' parameter for BatchByCount", BatchThreshold) return nil } thresholdValue, err := strconv.Atoi(batchThreshold) if err != nil { - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Could not parse '%s' to an int for '%s' parameter for BatchByCount: %s", batchThreshold, BatchThreshold, err.Error()) return nil @@ -461,46 +471,46 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a transform, err := transforms.NewBatchByCount(thresholdValue) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch case BatchByTime: timeInterval, ok := parameters[TimeInterval] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByTime", TimeInterval) + app.lc.Errorf("Could not find '%s' parameter for BatchByTime", TimeInterval) return nil } transform, err := transforms.NewBatchByTime(timeInterval) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch case BatchByTimeAndCount: timeInterval, ok := parameters[TimeInterval] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + TimeInterval) + app.lc.Error("Could not find " + TimeInterval) return nil } batchThreshold, ok := parameters[BatchThreshold] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BatchThreshold) + app.lc.Error("Could not find " + BatchThreshold) return nil } thresholdValue, err := strconv.Atoi(batchThreshold) if err != nil { - dynamic.Sdk.LoggingClient.Errorf("Could not parse '%s' to an int for '%s' parameter: %s", batchThreshold, BatchThreshold, err.Error()) + app.lc.Errorf("Could not parse '%s' to an int for '%s' parameter: %s", batchThreshold, BatchThreshold, err.Error()) } transform, err := transforms.NewBatchByTimeAndCount(timeInterval, thresholdValue) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid batch mode '%s'. Must be '%s', '%s' or '%s'", mode, BatchByCount, @@ -511,10 +521,10 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a } // JSONLogic ... -func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) JSONLogic(parameters map[string]string) interfaces.AppFunction { rule, ok := parameters[Rule] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + Rule) + app.lc.Error("Could not find " + Rule) return nil } @@ -524,10 +534,10 @@ func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]strin // AddTags adds the configured list of tags to Events passed to the transform. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) AddTags(parameters map[string]string) interfaces.AppFunction { tagsSpec, ok := parameters[Tags] if !ok { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not find '%s' parameter", Tags)) + app.lc.Error(fmt.Sprintf("Could not find '%s' parameter", Tags)) return nil } @@ -537,16 +547,16 @@ func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) for _, tag := range tagKeyValues { keyValue := util.DeleteEmptyAndTrim(strings.FieldsFunc(tag, util.SplitColon)) if len(keyValue) != 2 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec)) + app.lc.Error(fmt.Sprintf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec)) return nil } if len(keyValue[0]) == 0 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Tag key missing. Got '%s'", tag)) + app.lc.Error(fmt.Sprintf("Tag key missing. Got '%s'", tag)) return nil } if len(keyValue[1]) == 0 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Tag value missing. Got '%s'", tag)) + app.lc.Error(fmt.Sprintf("Tag value missing. Got '%s'", tag)) return nil } @@ -557,13 +567,13 @@ func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) return transform.AddTags } -func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( +func (app *Configurable) processFilterParameters( funcName string, parameters map[string]string, paramName string) (*transforms.Filter, bool) { names, ok := parameters[paramName] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for %s", paramName, funcName) + app.lc.Errorf("Could not find '%s' parameter for %s", paramName, funcName) return nil, false } @@ -573,7 +583,7 @@ func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( var err error filterOutBool, err = strconv.ParseBool(filterOut) if err != nil { - dynamic.Sdk.LoggingClient.Errorf("Could not convert filterOut value `%s` to bool for %s", filterOut, funcName) + app.lc.Errorf("Could not convert filterOut value `%s` to bool for %s", filterOut, funcName) return nil, false } } @@ -587,7 +597,7 @@ func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( return &transform, true } -func (dynamic AppFunctionsSDKConfigurable) processHttpExportParameters( +func (app *Configurable) processHttpExportParameters( parameters map[string]string) (*postPutParameters, error) { result := postPutParameters{} diff --git a/appsdk/configurable_test.go b/internal/app/configurable_test.go similarity index 88% rename from appsdk/configurable_test.go rename to internal/app/configurable_test.go index 32d1d7ba5..0804bedbc 100644 --- a/appsdk/configurable_test.go +++ b/internal/app/configurable_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package appsdk +package app import ( "net/http" @@ -24,11 +24,7 @@ import ( ) func TestFilterByProfileName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -54,11 +50,7 @@ func TestFilterByProfileName(t *testing.T) { } func TestFilterByDeviceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -84,11 +76,7 @@ func TestFilterByDeviceName(t *testing.T) { } func TestFilterBySourceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -114,11 +102,7 @@ func TestFilterBySourceName(t *testing.T) { } func TestFilterByResourceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -144,11 +128,7 @@ func TestFilterByResourceName(t *testing.T) { } func TestTransform(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { Name string @@ -171,11 +151,7 @@ func TestTransform(t *testing.T) { } func TestHTTPExport(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} testUrl := "http://url" testMimeType := clients.ContentTypeJSON @@ -254,11 +230,7 @@ func TestHTTPExport(t *testing.T) { } func TestSetOutputData(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -272,22 +244,18 @@ func TestSetOutputData(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - trx := configurable.SetOutputData(tt.params) + trx := configurable.SetResponseData(tt.params) if tt.expectNil { - assert.Nil(t, trx, "return result from SetOutputData should be nil") + assert.Nil(t, trx, "return result from SetResponseData should be nil") } else { - assert.NotNil(t, trx, "return result from SetOutputData should not be nil") + assert.NotNil(t, trx, "return result from SetResponseData should not be nil") } }) } } func TestBatchByCount(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByCount @@ -297,11 +265,7 @@ func TestBatchByCount(t *testing.T) { } func TestBatchByTime(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByTime @@ -311,11 +275,7 @@ func TestBatchByTime(t *testing.T) { } func TestBatchByTimeAndCount(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByTimeAndCount @@ -330,22 +290,15 @@ func TestJSONLogic(t *testing.T) { params := make(map[string]string) params[Rule] = "{}" - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} + trx := configurable.JSONLogic(params) assert.NotNil(t, trx, "return result from JSONLogic should not be nil") } func TestMQTTExport(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[BrokerAddress] = "mqtt://broker:8883" @@ -364,11 +317,7 @@ func TestMQTTExport(t *testing.T) { } func TestAddTags(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { Name string @@ -397,11 +346,7 @@ func TestAddTags(t *testing.T) { } func TestEncrypt(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} key := "xyz12345" vector := "1243565" diff --git a/internal/app/service.go b/internal/app/service.go new file mode 100644 index 000000000..936d61c3e --- /dev/null +++ b/internal/app/service.go @@ -0,0 +1,532 @@ +// +// Copyright (c) 2021 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 app + +import ( + "context" + "errors" + "fmt" + nethttp "net/http" + "os" + "os/signal" + "reflect" + "strings" + "sync" + "syscall" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-registry/v2/registry" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" + bootstrapHandlers "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/handlers" + bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/edgexfoundry/go-mod-messaging/v2/messaging" + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + "github.com/gorilla/mux" +) + +const ( + envProfile = "EDGEX_PROFILE" + envServiceKey = "EDGEX_SERVICE_KEY" + + optionalPasswordKey = "Password" +) + +// NewService create, initializes and returns new instance of app.Service which implements the +// interfaces.ApplicationService interface +func NewService(serviceKey string, targetType interface{}, profileSuffixPlaceholder string) *Service { + return &Service{ + serviceKey: serviceKey, + targetType: targetType, + profileSuffixPlaceholder: profileSuffixPlaceholder, + } +} + +// Service provides the necessary struct and functions to create an instance of the +// interfaces.ApplicationService interface. +type Service struct { + dic *di.Container + serviceKey string + targetType interface{} + config *common.ConfigurationStruct + lc logger.LoggingClient + transforms []interfaces.AppFunction + usingConfigurablePipeline bool + runtime *runtime.GolangRuntime + webserver *webserver.WebServer + ctx contextGroup + deferredFunctions []bootstrap.Deferred + backgroundPublishChannel <-chan types.MessageEnvelope + customTriggerFactories map[string]func(sdk *Service) (interfaces.Trigger, error) + profileSuffixPlaceholder string + commandLine commandLineFlags +} + +type commandLineFlags struct { + skipVersionCheck bool + serviceKeyOverride string +} + +type contextGroup struct { + storeForwardWg *sync.WaitGroup + storeForwardCancelCtx context.CancelFunc + appWg *sync.WaitGroup + appCtx context.Context + appCancelCtx context.CancelFunc + stop context.CancelFunc +} + +// AddRoute allows you to leverage the existing webserver to add routes. +func (svc *Service) AddRoute(route string, handler func(nethttp.ResponseWriter, *nethttp.Request), methods ...string) error { + if route == clients.ApiPingRoute || + route == clients.ApiConfigRoute || + route == clients.ApiMetricsRoute || + route == clients.ApiVersionRoute || + route == internal.ApiTriggerRoute { + return errors.New("route is reserved") + } + return svc.webserver.AddRoute(route, svc.addContext(handler), methods...) +} + +// AddBackgroundPublisher will create a channel of provided capacity to be +// consumed by the MessageBus output and return a publisher that writes to it +func (svc *Service) AddBackgroundPublisher(capacity int) interfaces.BackgroundPublisher { + bgchan, pub := newBackgroundPublisher(capacity) + svc.backgroundPublishChannel = bgchan + return pub +} + +// MakeItStop will force the service loop to exit in the same fashion as SIGINT/SIGTERM received from the OS +func (svc *Service) MakeItStop() { + if svc.ctx.stop != nil { + svc.ctx.stop() + } else { + svc.lc.Warn("MakeItStop called but no stop handler set on SDK - is the service running?") + } +} + +// MakeItRun initializes and starts the trigger as specified in the +// configuration. It will also configure the webserver and start listening on +// the specified port. +func (svc *Service) MakeItRun() error { + runCtx, stop := context.WithCancel(context.Background()) + + svc.ctx.stop = stop + + svc.runtime = &runtime.GolangRuntime{ + TargetType: svc.targetType, + ServiceKey: svc.serviceKey, + } + + svc.runtime.Initialize(svc.dic) + svc.runtime.SetTransforms(svc.transforms) + + // determine input type and create trigger for it + t := svc.setupTrigger(svc.config, svc.runtime) + if t == nil { + return errors.New("Failed to create Trigger") + } + + // Initialize the trigger (i.e. start a web server, or connect to message bus) + deferred, err := t.Initialize(svc.ctx.appWg, svc.ctx.appCtx, svc.backgroundPublishChannel) + if err != nil { + svc.lc.Error(err.Error()) + return errors.New("Failed to initialize Trigger") + } + + // deferred is a a function that needs to be called when services exits. + svc.addDeferred(deferred) + + if svc.config.Writable.StoreAndForward.Enabled { + svc.startStoreForward() + } else { + svc.lc.Info("StoreAndForward disabled. Not running retry loop.") + } + + svc.lc.Info(svc.config.Service.StartupMsg) + + signals := make(chan os.Signal) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + httpErrors := make(chan error) + defer close(httpErrors) + + svc.webserver.StartWebServer(httpErrors) + + select { + case httpError := <-httpErrors: + svc.lc.Info("Http error received: ", httpError.Error()) + err = httpError + + case signalReceived := <-signals: + svc.lc.Info("Terminating signal received: " + signalReceived.String()) + + case <-runCtx.Done(): + svc.lc.Info("Terminating: svc.MakeItStop called") + } + + svc.ctx.stop = nil + + if svc.config.Writable.StoreAndForward.Enabled { + svc.ctx.storeForwardCancelCtx() + svc.ctx.storeForwardWg.Wait() + } + + svc.ctx.appCancelCtx() // Cancel all long running go funcs + svc.ctx.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 svc.deferredFunctions { + deferredFunc() + } + + return err +} + +// LoadConfigurablePipeline sets the function pipeline from configuration +func (svc *Service) LoadConfigurablePipeline() ([]interfaces.AppFunction, error) { + var pipeline []interfaces.AppFunction + + svc.usingConfigurablePipeline = true + + svc.targetType = nil + + if svc.config.Writable.Pipeline.UseTargetTypeOfByteArray { + svc.targetType = &[]byte{} + } + + configurable := NewConfigurable(svc.lc) + + valueOfType := reflect.ValueOf(configurable) + pipelineConfig := svc.config.Writable.Pipeline + executionOrder := util.DeleteEmptyAndTrim(strings.FieldsFunc(pipelineConfig.ExecutionOrder, util.SplitComma)) + + if len(executionOrder) <= 0 { + return nil, errors.New( + "execution Order has 0 functions specified. You must have a least one function in the pipeline") + } + + svc.lc.Debugf("Function Pipeline Execution Order: [%s]", pipelineConfig.ExecutionOrder) + + for _, functionName := range executionOrder { + functionName = strings.TrimSpace(functionName) + configuration, ok := pipelineConfig.Functions[functionName] + if !ok { + return nil, fmt.Errorf("function '%s' configuration not found in Pipeline.Functions section", functionName) + } + + result := valueOfType.MethodByName(functionName) + if result.Kind() == reflect.Invalid { + return nil, fmt.Errorf("function %s is not a built in SDK function", functionName) + } else if result.IsNil() { + return nil, fmt.Errorf("invalid/missing configuration for %s", functionName) + } + + // determine number of parameters required for function call + inputParameters := make([]reflect.Value, result.Type().NumIn()) + // set keys to be all lowercase to avoid casing issues from configuration + for key := range configuration.Parameters { + value := configuration.Parameters[key] + delete(configuration.Parameters, key) // Make sure the old key has been removed so don't have multiples + configuration.Parameters[strings.ToLower(key)] = value + } + for index := range inputParameters { + parameter := result.Type().In(index) + + switch parameter { + case reflect.TypeOf(map[string]string{}): + inputParameters[index] = reflect.ValueOf(configuration.Parameters) + + default: + return nil, fmt.Errorf( + "function %s has an unsupported parameter type: %s", + functionName, + parameter.String(), + ) + } + } + + function, ok := result.Call(inputParameters)[0].Interface().(interfaces.AppFunction) + if !ok { + return nil, fmt.Errorf("failed to cast function %s as AppFunction type", functionName) + } + + if function == nil { + return nil, fmt.Errorf("%s from configuration failed", functionName) + } + + pipeline = append(pipeline, function) + svc.lc.Debugf( + "%s function added to configurable pipeline with parameters: [%s]", + functionName, + listParameters(configuration.Parameters)) + } + + return pipeline, nil +} + +// SetFunctionsPipeline sets the function pipeline to the list of specified functions in the order provided. +func (svc *Service) SetFunctionsPipeline(transforms ...interfaces.AppFunction) error { + if len(transforms) == 0 { + return errors.New("no transforms provided to pipeline") + } + + svc.transforms = transforms + + if svc.runtime != nil { + svc.runtime.SetTransforms(transforms) + svc.runtime.TargetType = svc.targetType + } + + return nil +} + +// ApplicationSettings returns the values specified in the custom configuration section. +func (svc *Service) ApplicationSettings() map[string]string { + return svc.config.ApplicationSettings +} + +// GetAppSettingStrings returns the strings slice for the specified App Setting. +func (svc *Service) GetAppSettingStrings(setting string) ([]string, error) { + if svc.config.ApplicationSettings == nil { + return nil, fmt.Errorf("%s setting not found: ApplicationSettings section is missing", setting) + } + + settingValue, ok := svc.config.ApplicationSettings[setting] + if !ok { + return nil, fmt.Errorf("%s setting not found in ApplicationSettings", setting) + } + + valueStrings := util.DeleteEmptyAndTrim(strings.FieldsFunc(settingValue, util.SplitComma)) + + return valueStrings, nil +} + +// Initialize bootstraps the service making it ready to accept functions for the pipeline and to run the configured trigger. +func (svc *Service) Initialize() error { + startupTimer := startup.NewStartUpTimer(svc.serviceKey) + + additionalUsage := + " -s/--skipVersionCheck Indicates the service should skip the Core Service's version compatibility check.\n" + + " -sk/--serviceKey Overrides the service service key used with Registry and/or Configuration Providers.\n" + + " If the name provided contains the text ``, this text will be replaced with\n" + + " the name of the profile used." + + sdkFlags := flags.NewWithUsage(additionalUsage) + sdkFlags.FlagSet.BoolVar(&svc.commandLine.skipVersionCheck, "skipVersionCheck", false, "") + sdkFlags.FlagSet.BoolVar(&svc.commandLine.skipVersionCheck, "s", false, "") + sdkFlags.FlagSet.StringVar(&svc.commandLine.serviceKeyOverride, "serviceKey", "", "") + sdkFlags.FlagSet.StringVar(&svc.commandLine.serviceKeyOverride, "sk", "", "") + + sdkFlags.Parse(os.Args[1:]) + + // Temporarily setup logging to STDOUT so the client can be used before bootstrapping is completed + svc.lc = logger.NewClient(svc.serviceKey, models.InfoLog) + + svc.setServiceKey(sdkFlags.Profile()) + + svc.lc.Info(fmt.Sprintf("Starting %s %s ", svc.serviceKey, internal.ApplicationVersion)) + + svc.config = &common.ConfigurationStruct{} + svc.dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return svc.config + }, + }) + + svc.ctx.appCtx, svc.ctx.appCancelCtx = context.WithCancel(context.Background()) + svc.ctx.appWg = &sync.WaitGroup{} + + var deferred bootstrap.Deferred + var successful bool + var configUpdated config.UpdatedStream = make(chan struct{}) + + svc.ctx.appWg, deferred, successful = bootstrap.RunAndReturnWaitGroup( + svc.ctx.appCtx, + svc.ctx.appCancelCtx, + sdkFlags, + svc.serviceKey, + internal.ConfigRegistryStem, + svc.config, + configUpdated, + startupTimer, + svc.dic, + []bootstrapInterfaces.BootstrapHandler{ + bootstrapHandlers.SecureProviderBootstrapHandler, + handlers.NewDatabase().BootstrapHandler, + handlers.NewClients().BootstrapHandler, + handlers.NewTelemetry().BootstrapHandler, + handlers.NewVersionValidator(svc.commandLine.skipVersionCheck, internal.SDKVersion).BootstrapHandler, + }, + ) + + // deferred is a a function that needs to be called when services exits. + svc.addDeferred(deferred) + + if !successful { + return fmt.Errorf("boostrapping failed") + } + + // Bootstrapping is complete, so now need to retrieve the needed objects from the containers. + svc.lc = bootstrapContainer.LoggingClientFrom(svc.dic.Get) + + // If using the RedisStreams MessageBus implementation then need to make sure the + // password for the Redis DB is set in the MessageBus Optional properties. + triggerType := strings.ToUpper(svc.config.Trigger.Type) + if triggerType == TriggerTypeMessageBus && + svc.config.Trigger.EdgexMessageBus.Type == messaging.RedisStreams { + + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + credentials, err := secretProvider.GetSecrets(svc.config.Database.Type) + if err != nil { + return fmt.Errorf("unable to set RedisStreams password from DB credentials") + } + svc.config.Trigger.EdgexMessageBus.Optional[optionalPasswordKey] = credentials[secret.PasswordKey] + } + + // 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(svc).WaitForConfigUpdates(configUpdated) + + svc.webserver = webserver.NewWebServer(svc.dic, mux.NewRouter()) + svc.webserver.ConfigureStandardRoutes() + + svc.lc.Info("Service started in: " + startupTimer.SinceAsString()) + + return nil +} + +// GetSecret retrieves secret data from the secret store at the specified path. +func (svc *Service) GetSecret(path string, keys ...string) (map[string]string, error) { + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + return secretProvider.GetSecrets(path, keys...) +} + +// StoreSecret stores the secret data to a secret store at the specified path. +func (svc *Service) StoreSecret(path string, secretData map[string]string) error { + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + return secretProvider.StoreSecrets(path, secretData) +} + +// LoggingClient returns the Logging client from the dependency injection container +func (svc *Service) LoggingClient() logger.LoggingClient { + return svc.lc +} + +// RegistryClient returns the Registry client, which may be nil, from the dependency injection container +func (svc *Service) RegistryClient() registry.Client { + return bootstrapContainer.RegistryFrom(svc.dic.Get) +} + +// EventClient returns the Event client, which may be nil, from the dependency injection container +func (svc *Service) EventClient() coredata.EventClient { + return container.EventClientFrom(svc.dic.Get) +} + +// CommandClient returns the Command client, which may be nil, from the dependency injection container +func (svc *Service) CommandClient() command.CommandClient { + return container.CommandClientFrom(svc.dic.Get) +} + +// NotificationsClient returns the Notifications client, which may be nil, from the dependency injection container +func (svc *Service) NotificationsClient() notifications.NotificationsClient { + return container.NotificationsClientFrom(svc.dic.Get) +} + +func listParameters(parameters map[string]string) string { + result := "" + first := true + for key, value := range parameters { + if first { + result = fmt.Sprintf("%s='%s'", key, value) + first = false + continue + } + + result += fmt.Sprintf(", %s='%s'", key, value) + } + + return result +} + +func (svc *Service) addContext(next func(nethttp.ResponseWriter, *nethttp.Request)) func(nethttp.ResponseWriter, *nethttp.Request) { + return func(w nethttp.ResponseWriter, r *nethttp.Request) { + ctx := context.WithValue(r.Context(), interfaces.AppServiceContextKey, svc) + next(w, r.WithContext(ctx)) + } +} + +func (svc *Service) addDeferred(deferred bootstrap.Deferred) { + if deferred != nil { + svc.deferredFunctions = append(svc.deferredFunctions, deferred) + } +} + +func (svc *Service) setServiceKey(profile string) { + envValue := os.Getenv(envServiceKey) + if len(envValue) > 0 { + svc.commandLine.serviceKeyOverride = envValue + svc.lc.Info( + fmt.Sprintf("Environment profileOverride of '-n/--serviceName' by environment variable: %s=%s", + envServiceKey, + envValue)) + } + + // serviceKeyOverride may have been set by the -n/--serviceName command-line option and not the environment variable + if len(svc.commandLine.serviceKeyOverride) > 0 { + svc.serviceKey = svc.commandLine.serviceKeyOverride + } + + if !strings.Contains(svc.serviceKey, svc.profileSuffixPlaceholder) { + // No placeholder, so nothing to do here + return + } + + // Have to handle environment override here before common bootstrap is used so it is passed the proper service key + profileOverride := os.Getenv(envProfile) + if len(profileOverride) > 0 { + profile = profileOverride + } + + if len(profile) > 0 { + svc.serviceKey = strings.Replace(svc.serviceKey, svc.profileSuffixPlaceholder, profile, 1) + return + } + + // No profile specified so remove the placeholder text + svc.serviceKey = strings.Replace(svc.serviceKey, svc.profileSuffixPlaceholder, "", 1) +} diff --git a/appsdk/sdk_test.go b/internal/app/service_test.go similarity index 80% rename from appsdk/sdk_test.go rename to internal/app/service_test.go index f828e4d77..e1ad9d310 100644 --- a/appsdk/sdk_test.go +++ b/internal/app/service_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // limitations under the License. // -package appsdk +package app import ( "fmt" @@ -23,13 +23,16 @@ import ( "reflect" "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" triggerHttp "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/gorilla/mux" @@ -38,10 +41,20 @@ import ( ) var lc logger.LoggingClient +var dic *di.Container func TestMain(m *testing.M) { // No remote and no file results in STDOUT logging only lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + }) + m.Run() } @@ -51,9 +64,10 @@ func IsInstanceOf(objectPtr, typePtr interface{}) bool { func TestAddRoute(t *testing.T) { router := mux.NewRouter() - ws := webserver.NewWebServer(&common.ConfigurationStruct{}, nil, lc, router) - sdk := AppFunctionsSDK{ + ws := webserver.NewWebServer(dic, router) + + sdk := Service{ webserver: ws, } _ = sdk.AddRoute("/test", func(http.ResponseWriter, *http.Request) {}, http.MethodGet) @@ -69,7 +83,7 @@ func TestAddRoute(t *testing.T) { } func TestAddBackgroundPublisher(t *testing.T) { - sdk := AppFunctionsSDK{} + sdk := Service{} pub, ok := sdk.AddBackgroundPublisher(1).(*backgroundPublisher) if !ok { @@ -77,16 +91,16 @@ func TestAddBackgroundPublisher(t *testing.T) { } require.NotNil(t, pub.output, "publisher should have an output channel set") - require.NotNil(t, sdk.backgroundChannel, "sdk should have a background channel set for passing to trigger initialization") + require.NotNil(t, sdk.backgroundPublishChannel, "svc should have a background channel set for passing to trigger initialization") // compare addresses since types will not match - assert.Equal(t, fmt.Sprintf("%p", sdk.backgroundChannel), fmt.Sprintf("%p", pub.output), + assert.Equal(t, fmt.Sprintf("%p", sdk.backgroundPublishChannel), fmt.Sprintf("%p", pub.output), "same channel should be referenced by the BackgroundPublisher and the SDK.") } func TestSetupHTTPTrigger(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: "htTp", @@ -94,7 +108,7 @@ func TestSetupHTTPTrigger(t *testing.T) { }, } testRuntime := &runtime.GolangRuntime{} - testRuntime.Initialize(nil, nil) + testRuntime.Initialize(dic) testRuntime.SetTransforms(sdk.transforms) trigger := sdk.setupTrigger(sdk.config, testRuntime) result := IsInstanceOf(trigger, (*triggerHttp.Trigger)(nil)) @@ -102,8 +116,8 @@ func TestSetupHTTPTrigger(t *testing.T) { } func TestSetupMessageBusTrigger(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, @@ -111,7 +125,7 @@ func TestSetupMessageBusTrigger(t *testing.T) { }, } testRuntime := &runtime.GolangRuntime{} - testRuntime.Initialize(nil, nil) + testRuntime.Initialize(dic) testRuntime.SetTransforms(sdk.transforms) trigger := sdk.setupTrigger(sdk.config, testRuntime) result := IsInstanceOf(trigger, (*messagebus.Trigger)(nil)) @@ -119,8 +133,8 @@ func TestSetupMessageBusTrigger(t *testing.T) { } func TestSetFunctionsPipelineNoTransforms(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, @@ -133,20 +147,20 @@ func TestSetFunctionsPipelineNoTransforms(t *testing.T) { } func TestSetFunctionsPipelineOneTransform(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - runtime: &runtime.GolangRuntime{}, + sdk := Service{ + lc: lc, + runtime: &runtime.GolangRuntime{}, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, }, }, } - function := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + function := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return true, nil } - sdk.runtime.Initialize(nil, nil) + sdk.runtime.Initialize(dic) err := sdk.SetFunctionsPipeline(function) require.NoError(t, err) assert.Equal(t, 1, len(sdk.transforms)) @@ -156,7 +170,7 @@ func TestApplicationSettings(t *testing.T) { expectedSettingKey := "ApplicationName" expectedSettingValue := "simple-filter-xml" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "ApplicationName": "simple-filter-xml", @@ -173,7 +187,7 @@ func TestApplicationSettings(t *testing.T) { } func TestApplicationSettingsNil(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{}, } @@ -185,7 +199,7 @@ func TestGetAppSettingStrings(t *testing.T) { setting := "DeviceNames" expected := []string{"dev1", "dev2"} - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "DeviceNames": "dev1, dev2", @@ -202,7 +216,7 @@ func TestGetAppSettingStringsSettingMissing(t *testing.T) { setting := "DeviceNames" expected := "setting not found in ApplicationSettings" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{}, }, @@ -217,7 +231,7 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { setting := "DeviceNames" expected := "ApplicationSettings section is missing" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{}, } @@ -227,8 +241,8 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { } func TestLoadConfigurablePipelineFunctionNotFound(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ @@ -249,8 +263,8 @@ func TestLoadConfigurablePipelineNotABuiltInSdkFunction(t *testing.T) { functions := make(map[string]common.PipelineFunction) functions["Bogus"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ @@ -275,14 +289,14 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { functions["Transform"] = common.PipelineFunction{ Parameters: map[string]string{TransformType: TransformXml}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "FilterByDeviceName, Transform, SetOutputData", + ExecutionOrder: "FilterByDeviceName, Transform, SetResponseData", Functions: functions, }, }, @@ -300,14 +314,14 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { functions["Compress"] = common.PipelineFunction{ Parameters: map[string]string{Algorithm: CompressGZIP}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "Compress, SetOutputData", + ExecutionOrder: "Compress, SetResponseData", UseTargetTypeOfByteArray: true, Functions: functions, }, @@ -317,9 +331,9 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { _, err := sdk.LoadConfigurablePipeline() require.NoError(t, err) - require.NotNil(t, sdk.TargetType) - assert.Equal(t, reflect.Ptr, reflect.TypeOf(sdk.TargetType).Kind()) - assert.Equal(t, reflect.TypeOf([]byte{}).Kind(), reflect.TypeOf(sdk.TargetType).Elem().Kind()) + require.NotNil(t, sdk.targetType) + assert.Equal(t, reflect.Ptr, reflect.TypeOf(sdk.targetType).Kind()) + assert.Equal(t, reflect.TypeOf([]byte{}).Kind(), reflect.TypeOf(sdk.targetType).Elem().Kind()) } func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { @@ -327,14 +341,14 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { functions["Compress"] = common.PipelineFunction{ Parameters: map[string]string{Algorithm: CompressGZIP}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "Compress, SetOutputData", + ExecutionOrder: "Compress, SetResponseData", UseTargetTypeOfByteArray: false, Functions: functions, }, @@ -344,13 +358,14 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { _, err := sdk.LoadConfigurablePipeline() require.NoError(t, err) - assert.Nil(t, sdk.TargetType) + assert.Nil(t, sdk.targetType) } func TestSetServiceKey(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - ServiceKey: "MyAppService", + sdk := Service{ + lc: lc, + serviceKey: "MyAppService", + profileSuffixPlaceholder: interfaces.ProfileSuffixPlaceholder, } tests := []struct { @@ -365,13 +380,13 @@ func TestSetServiceKey(t *testing.T) { }{ { name: "No profile", - originalServiceKey: "MyAppService" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService", }, { name: "Profile specified, no override", profile: "mqtt-export", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-mqtt-export", }, { @@ -379,14 +394,14 @@ func TestSetServiceKey(t *testing.T) { profile: "rules-engine", profileEnvVar: envProfile, profileEnvValue: "rules-engine-redis", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-rules-engine-redis", }, { name: "No profile specified with V2 override", profileEnvVar: envProfile, profileEnvValue: "http-export", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-http-export", }, { @@ -446,13 +461,13 @@ func TestSetServiceKey(t *testing.T) { defer os.Clearenv() if len(test.serviceKeyCommandLineOverride) > 0 { - sdk.serviceKeyOverride = test.serviceKeyCommandLineOverride + sdk.commandLine.serviceKeyOverride = test.serviceKeyCommandLineOverride } - sdk.ServiceKey = test.originalServiceKey + sdk.serviceKey = test.originalServiceKey sdk.setServiceKey(test.profile) - assert.Equal(t, test.expectedServiceKey, sdk.ServiceKey) + assert.Equal(t, test.expectedServiceKey, sdk.serviceKey) }) } } @@ -460,16 +475,18 @@ func TestSetServiceKey(t *testing.T) { func TestMakeItStop(t *testing.T) { stopCalled := false - sdk := AppFunctionsSDK{ - stop: func() { - stopCalled = true + sdk := Service{ + ctx: contextGroup{ + stop: func() { + stopCalled = true + }, }, - LoggingClient: logger.NewMockClient(), + lc: logger.NewMockClient(), } sdk.MakeItStop() - require.True(t, stopCalled, "Cancel function set at sdk.stop should be called if set") + require.True(t, stopCalled, "Cancel function set at svc.stop should be called if set") - sdk.stop = nil + sdk.ctx.stop = nil sdk.MakeItStop() //should avoid nil pointer } diff --git a/internal/app/triggerfactory.go b/internal/app/triggerfactory.go new file mode 100644 index 000000000..85c0f2c22 --- /dev/null +++ b/internal/app/triggerfactory.go @@ -0,0 +1,121 @@ +// +// Copyright (c) 2020 Technocrats +// Copyright (c) 2021 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 app + +import ( + "errors" + "fmt" + "strings" + + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) + +const ( + // Valid types of App Service triggers + TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" + TriggerTypeMQTT = "EXTERNAL-MQTT" + TriggerTypeHTTP = "HTTP" +) + +// RegisterCustomTriggerFactory allows users to register builders for custom trigger types +func (svc *Service) RegisterCustomTriggerFactory(name string, + factory func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error { + nu := strings.ToUpper(name) + + if nu == TriggerTypeMessageBus || + nu == TriggerTypeHTTP || + nu == TriggerTypeMQTT { + return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + } + + if svc.customTriggerFactories == nil { + svc.customTriggerFactories = make(map[string]func(sdk *Service) (interfaces.Trigger, error), 1) + } + + svc.customTriggerFactories[nu] = func(sdk *Service) (interfaces.Trigger, error) { + return factory(interfaces.TriggerConfig{ + Config: sdk.config.Trigger.EdgexMessageBus, + Logger: sdk.lc, + ContextBuilder: sdk.defaultTriggerContextBuilder, + MessageProcessor: sdk.defaultTriggerMessageProcessor, + }) + } + + return nil +} + +func (svc *Service) defaultTriggerMessageProcessor(appContext interfaces.AppFunctionContext, envelope types.MessageEnvelope) error { + context, ok := appContext.(*appfunction.Context) + if !ok { + return fmt.Errorf("App Context must be an istance of internal appfunction.Context. Use NewAppContext to create instance.") + } + + messageError := svc.runtime.ProcessMessage(context, envelope) + if messageError != nil { + // ProcessMessage logs the error, so no need to log it here. + return messageError.Err + } + + return nil +} + +func (svc *Service) defaultTriggerContextBuilder(env types.MessageEnvelope) interfaces.AppFunctionContext { + return appfunction.NewContext(env.CorrelationID, svc.dic, env.ContentType) +} + +func (svc *Service) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) interfaces.Trigger { + var t interfaces.Trigger + // Need to make dynamic, search for the trigger that is input + + switch triggerType := strings.ToUpper(configuration.Trigger.Type); triggerType { + case TriggerTypeHTTP: + svc.LoggingClient().Info("HTTP trigger selected") + t = http.NewTrigger(svc.dic, runtime, svc.webserver) + + case TriggerTypeMessageBus: + svc.LoggingClient().Info("EdgeX MessageBus trigger selected") + t = messagebus.NewTrigger(svc.dic, runtime) + + case TriggerTypeMQTT: + svc.LoggingClient().Info("External MQTT trigger selected") + t = mqtt.NewTrigger(svc.dic, runtime) + + default: + if factory, found := svc.customTriggerFactories[triggerType]; found { + var err error + t, err = factory(svc) + if err != nil { + svc.LoggingClient().Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) + return nil + } + } else { + svc.LoggingClient().Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type)) + } + } + + return t +} diff --git a/appsdk/triggerfactory_test.go b/internal/app/triggerfactory_test.go similarity index 81% rename from appsdk/triggerfactory_test.go rename to internal/app/triggerfactory_test.go index d862ab989..1624bb34a 100644 --- a/appsdk/triggerfactory_test.go +++ b/internal/app/triggerfactory_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +15,7 @@ // limitations under the License. // -package appsdk +package app import ( "context" @@ -23,10 +24,14 @@ import ( "sync" "testing" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" @@ -39,7 +44,7 @@ import ( func TestRegisterCustomTriggerFactory_HTTP(t *testing.T) { name := strings.ToTitle(TriggerTypeHTTP) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -49,7 +54,7 @@ func TestRegisterCustomTriggerFactory_HTTP(t *testing.T) { func TestRegisterCustomTriggerFactory_EdgeXMessageBus(t *testing.T) { name := strings.ToTitle(TriggerTypeMessageBus) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -59,7 +64,7 @@ func TestRegisterCustomTriggerFactory_EdgeXMessageBus(t *testing.T) { func TestRegisterCustomTriggerFactory_MQTT(t *testing.T) { name := strings.ToTitle(TriggerTypeMQTT) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -70,10 +75,11 @@ func TestRegisterCustomTrigger(t *testing.T) { name := "cUsToM tRiGgEr" trig := mockCustomTrigger{} - builder := func(c TriggerConfig) (Trigger, error) { + builder := func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &trig, nil } - sdk := AppFunctionsSDK{} + sdk := Service{config: &common.ConfigurationStruct{}} + err := sdk.RegisterCustomTriggerFactory(name, builder) require.Nil(t, err, "should not throw error") @@ -88,13 +94,13 @@ func TestRegisterCustomTrigger(t *testing.T) { } func TestSetupTrigger_HTTP(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeHTTP, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -104,13 +110,13 @@ func TestSetupTrigger_HTTP(t *testing.T) { } func TestSetupTrigger_EdgeXMessageBus(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -120,13 +126,22 @@ func TestSetupTrigger_EdgeXMessageBus(t *testing.T) { } func TestSetupTrigger_MQTT(t *testing.T) { - sdk := AppFunctionsSDK{ - config: &common.ConfigurationStruct{ - Trigger: common.TriggerInfo{ - Type: TriggerTypeMQTT, - }, + config := &common.ConfigurationStruct{ + Trigger: common.TriggerInfo{ + Type: TriggerTypeMQTT, }, - LoggingClient: logger.MockLogger{}, + } + + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return config + }, + }) + + sdk := Service{ + dic: dic, + config: config, + lc: lc, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -145,16 +160,16 @@ func (*mockCustomTrigger) Initialize(_ *sync.WaitGroup, _ context.Context, _ <-c func TestSetupTrigger_CustomType(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } - err := sdk.RegisterCustomTriggerFactory(triggerName, func(c TriggerConfig) (Trigger, error) { + err := sdk.RegisterCustomTriggerFactory(triggerName, func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &mockCustomTrigger{}, nil }) require.NoError(t, err) @@ -168,16 +183,16 @@ func TestSetupTrigger_CustomType(t *testing.T) { func TestSetupTrigger_CustomType_Error(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } - err := sdk.RegisterCustomTriggerFactory(triggerName, func(c TriggerConfig) (Trigger, error) { + err := sdk.RegisterCustomTriggerFactory(triggerName, func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &mockCustomTrigger{}, errors.New("this should force returning nil even though we'll have a value") }) require.NoError(t, err) @@ -190,13 +205,13 @@ func TestSetupTrigger_CustomType_Error(t *testing.T) { func TestSetupTrigger_CustomType_NotFound(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) diff --git a/internal/appfunction/context.go b/internal/appfunction/context.go new file mode 100644 index 000000000..901630db0 --- /dev/null +++ b/internal/appfunction/context.go @@ -0,0 +1,220 @@ +// +// Copyright (c) 2021 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 appfunction + +import ( + "context" + "fmt" + "time" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" + + "github.com/google/uuid" +) + +// NewContext creates, initializes and return a new Context with implements the interfaces.AppFunctionContext interface +func NewContext(correlationID string, dic *di.Container, inputContentType string) *Context { + return &Context{ + correlationID: correlationID, + dic: dic, + inputContentType: inputContentType, + } +} + +// Context contains the data functions that implement the interfaces.AppFunctionContext +type Context struct { + dic *di.Container + correlationID string + inputContentType string + responseData []byte + retryData []byte + responseContentType string +} + +// SetCorrelationID sets the correlationID. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) SetCorrelationID(id string) { + appContext.correlationID = id +} + +// CorrelationID returns context's the correlation ID +func (appContext *Context) CorrelationID() string { + return appContext.correlationID +} + +// SetInputContentType sets the inputContentType. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) SetInputContentType(contentType string) { + appContext.inputContentType = contentType +} + +// InputContentType returns the context's inputContentType +func (appContext *Context) InputContentType() string { + return appContext.inputContentType +} + +// SetResponseData provides a way to return the specified data as a response to the trigger that initiated +// the execution of the function pipeline. In the case of an HTTP Trigger, the data will be returned as the http response. +// In the case of a message bus trigger, the data will be published to the configured message bus publish topic. +func (appContext *Context) SetResponseData(output []byte) { + appContext.responseData = output +} + +// ResponseData returns the context's responseData. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) ResponseData() []byte { + return appContext.responseData +} + +// SetResponseContentType sets the context's responseContentType +func (appContext *Context) SetResponseContentType(contentType string) { + appContext.responseContentType = contentType +} + +// ResponseContentType returns the context's responseContentType +func (appContext *Context) ResponseContentType() string { + return appContext.responseContentType +} + +// SetRetryData sets the context's retryData to the specified payload to be stored for later retry +// when the pipeline function returns an error. +func (appContext *Context) SetRetryData(payload []byte) { + appContext.retryData = payload +} + +// RetryData returns the context's retryData. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) RetryData() []byte { + return appContext.retryData +} + +// PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. +// TODO: This function must be reworked for the new V2 Event Client +func (appContext *Context) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { + lc := appContext.LoggingClient() + lc.Debug("Pushing to CoreData") + + if appContext.EventClient() == nil { + return nil, fmt.Errorf("unable to Push To CoreData: '%s' is missing from Clients configuration", handlers.CoreDataClientName) + } + + now := time.Now().UnixNano() + val, err := util.CoerceType(value) + if err != nil { + return nil, err + } + + // Temporary use V1 Reading until V2 EventClient is available + // TODO: Change to use dtos.Reading + v1Reading := models.Reading{ + Value: string(val), + ValueType: v2.ValueTypeString, + Origin: now, + Device: deviceName, + Name: readingName, + } + + readings := make([]models.Reading, 0, 1) + readings = append(readings, v1Reading) + + // Temporary use V1 Event until V2 EventClient is available + // TODO: Change to use dtos.Event + v1Event := &models.Event{ + Device: deviceName, + Origin: now, + Readings: readings, + } + + correlation := uuid.New().String() + ctx := context.WithValue(context.Background(), clients.CorrelationHeader, correlation) + result, err := appContext.EventClient().Add(ctx, v1Event) // TODO: Update to use V2 EventClient + if err != nil { + return nil, err + } + v1Event.ID = result + + // TODO: Remove once V2 EventClient is available + v2Reading := dtos.BaseReading{ + Versionable: commonDTO.NewVersionable(), + Id: v1Reading.Id, + Created: v1Reading.Created, + Origin: v1Reading.Origin, + DeviceName: v1Reading.Device, + ResourceName: v1Reading.Name, + ProfileName: "", + ValueType: v1Reading.ValueType, + SimpleReading: dtos.SimpleReading{Value: v1Reading.Value}, + } + + // TODO: Remove once V2 EventClient is available + v2Event := dtos.Event{ + Versionable: commonDTO.NewVersionable(), + Id: result, + DeviceName: v1Event.Device, + Origin: v1Event.Origin, + Readings: []dtos.BaseReading{v2Reading}, + } + return &v2Event, nil +} + +// GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. +func (appContext *Context) GetSecret(path string, keys ...string) (map[string]string, error) { + secretProvider := bootstrapContainer.SecretProviderFrom(appContext.dic.Get) + return secretProvider.GetSecrets(path, keys...) +} + +// SecretsLastUpdated returns that timestamp for when the secrets in the SecretStore where last updated. +func (appContext *Context) SecretsLastUpdated() time.Time { + secretProvider := bootstrapContainer.SecretProviderFrom(appContext.dic.Get) + return secretProvider.SecretsLastUpdated() +} + +// LoggingClient returns the Logging client from the dependency injection container +func (appContext *Context) LoggingClient() logger.LoggingClient { + return bootstrapContainer.LoggingClientFrom(appContext.dic.Get) +} + +// EventClient returns the Event client, which may be nil, from the dependency injection container +func (appContext *Context) EventClient() coredata.EventClient { + return container.EventClientFrom(appContext.dic.Get) +} + +// CommandClient returns the Command client, which may be nil, from the dependency injection container +func (appContext *Context) CommandClient() command.CommandClient { + return container.CommandClientFrom(appContext.dic.Get) +} + +// NotificationsClient returns the Notifications client, which may be nil, from the dependency injection container +func (appContext *Context) NotificationsClient() notifications.NotificationsClient { + return container.NotificationsClientFrom(appContext.dic.Get) + +} diff --git a/internal/appfunction/context_test.go b/internal/appfunction/context_test.go new file mode 100644 index 000000000..b55f94f64 --- /dev/null +++ b/internal/appfunction/context_test.go @@ -0,0 +1,265 @@ +// +// Copyright (c) 2021 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 appfunction + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" +) + +var target *Context +var dic *di.Container + +func TestMain(m *testing.M) { + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.EventClientName: func(get di.Get) interface{} { + return coredata.NewEventClient(local.New(clients.ApiEventRoute)) + }, + container.NotificationsClientName: func(get di.Get) interface{} { + return notifications.NewNotificationsClient(local.New(clients.ApiNotificationRoute)) + }, + container.CommandClientName: func(get di.Get) interface{} { + return command.NewCommandClient(local.New(clients.ApiCommandRoute)) + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + + }, + }) + target = NewContext("", dic, "") + + os.Exit(m.Run()) +} + +func TestContext_PushToCoreData(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("newId")) + if r.Method != http.MethodPost { + t.Errorf("expected http method is POST, active http method is : %s", r.Method) + } + url := clients.ApiEventRoute + if r.URL.EscapedPath() != url { + t.Errorf("expected uri path is %s, actual uri path is %s", url, r.URL.EscapedPath()) + } + })) + + defer ts.Close() + + eventClient := coredata.NewEventClient(local.New(ts.URL + clients.ApiEventRoute)) + dic.Update(di.ServiceConstructorMap{ + container.EventClientName: func(get di.Get) interface{} { + return eventClient + }, + }) + + expectedEvent := &dtos.Event{ + Versionable: common.NewVersionable(), + DeviceName: "device-name", + Readings: []dtos.BaseReading{ + { + Versionable: common.NewVersionable(), + DeviceName: "device-name", + ResourceName: "device-resource", + ValueType: v2.ValueTypeString, + SimpleReading: dtos.SimpleReading{ + Value: "value", + }, + }, + }, + } + actualEvent, err := target.PushToCoreData("device-name", "device-resource", "value") + require.NoError(t, err) + + assert.NotNil(t, actualEvent) + assert.Equal(t, expectedEvent.ApiVersion, actualEvent.ApiVersion) + assert.Equal(t, expectedEvent.DeviceName, actualEvent.DeviceName) + assert.True(t, len(expectedEvent.Readings) == 1) + assert.Equal(t, expectedEvent.Readings[0].DeviceName, actualEvent.Readings[0].DeviceName) + assert.Equal(t, expectedEvent.Readings[0].ResourceName, actualEvent.Readings[0].ResourceName) + assert.Equal(t, expectedEvent.Readings[0].Value, actualEvent.Readings[0].Value) + assert.Equal(t, expectedEvent.Readings[0].ValueType, actualEvent.Readings[0].ValueType) + assert.Equal(t, expectedEvent.Readings[0].ApiVersion, actualEvent.Readings[0].ApiVersion) +} + +func TestContext_CommandClient(t *testing.T) { + actual := target.CommandClient() + assert.NotNil(t, actual) +} + +func TestContext_EventClient(t *testing.T) { + actual := target.EventClient() + assert.NotNil(t, actual) +} + +func TestContext_LoggingClient(t *testing.T) { + actual := target.LoggingClient() + assert.NotNil(t, actual) +} + +func TestContext_NotificationsClient(t *testing.T) { + actual := target.NotificationsClient() + assert.NotNil(t, actual) +} + +func TestContext_CorrelationID(t *testing.T) { + expected := "123-3456" + target.correlationID = expected + + actual := target.CorrelationID() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetCorrelationID(t *testing.T) { + expected := "567-098" + + target.SetCorrelationID(expected) + actual := target.correlationID + + assert.Equal(t, expected, actual) +} + +func TestContext_InputContentType(t *testing.T) { + expected := clients.ContentTypeXML + target.inputContentType = expected + + actual := target.InputContentType() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetInputContentType(t *testing.T) { + expected := clients.ContentTypeCBOR + + target.SetInputContentType(expected) + actual := target.inputContentType + + assert.Equal(t, expected, actual) +} + +func TestContext_ResponseContentType(t *testing.T) { + expected := clients.ContentTypeJSON + target.responseContentType = expected + + actual := target.ResponseContentType() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetResponseContentType(t *testing.T) { + expected := clients.ContentTypeText + + target.SetResponseContentType(expected) + actual := target.responseContentType + + assert.Equal(t, expected, actual) +} + +func TestContext_SetResponseData(t *testing.T) { + expected := []byte("response data") + + target.SetResponseData(expected) + actual := target.responseData + + assert.Equal(t, expected, actual) +} + +func TestContext_ResponseData(t *testing.T) { + expected := []byte("response data") + target.responseData = expected + + actual := target.ResponseData() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetRetryData(t *testing.T) { + expected := []byte("retry data") + + target.SetRetryData(expected) + actual := target.retryData + + assert.Equal(t, expected, actual) +} + +func TestContext_RetryData(t *testing.T) { + expected := []byte("retry data") + target.retryData = expected + + actual := target.RetryData() + + assert.Equal(t, expected, actual) +} + +func TestContext_GetSecret(t *testing.T) { + // setup mock secret client + expected := map[string]string{ + "username": "TEST_USER", + "password": "TEST_PASS", + } + + mockSecretProvider := &mocks.SecretProvider{} + mockSecretProvider.On("GetSecrets", "mqtt").Return(expected, nil) + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + actual, err := target.GetSecret("mqtt") + require.NoError(t, err) + assert.Equal(t, expected, actual) +} + +func TestContext_SecretsLastUpdated(t *testing.T) { + expected := time.Now() + mockSecretProvider := &mocks.SecretProvider{} + mockSecretProvider.On("SecretsLastUpdated").Return(expected, nil) + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + actual := target.SecretsLastUpdated() + assert.Equal(t, expected, actual) +} diff --git a/internal/bootstrap/container/config.go b/internal/bootstrap/container/config.go index 6ba6dd311..55fc48ba7 100644 --- a/internal/bootstrap/container/config.go +++ b/internal/bootstrap/container/config.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package container import ( - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) // 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. +// ConfigurationFrom helper function queries the DIC and returns service's common.ConfigurationStruct implementation. func ConfigurationFrom(get di.Get) *common.ConfigurationStruct { return get(ConfigurationName).(*common.ConfigurationStruct) } diff --git a/internal/bootstrap/handlers/clients.go b/internal/bootstrap/handlers/clients.go index caab73d6b..96c64cd01 100644 --- a/internal/bootstrap/handlers/clients.go +++ b/internal/bootstrap/handlers/clients.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,7 +28,12 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" +) + +const ( + CoreCommandClientName = "Command" + CoreDataClientName = "CoreData" + NotificationsClientName = "Notifications" ) // Clients contains references to dependencies required by the Clients bootstrap implementation. @@ -42,9 +47,9 @@ func NewClients() *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, + _ context.Context, + _ *sync.WaitGroup, + _ startup.Timer, dic *di.Container) bool { config := container.ConfigurationFrom(dic.Get) @@ -56,22 +61,22 @@ func (_ *Clients) BootstrapHandler( // 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 { + if _, ok := config.Clients[CoreDataClientName]; ok { eventClient = coredata.NewEventClient( - local.New(config.Clients[common.CoreDataClientName].Url() + clients.ApiEventRoute)) + local.New(config.Clients[CoreDataClientName].Url() + clients.ApiEventRoute)) valueDescriptorClient = coredata.NewValueDescriptorClient( - local.New(config.Clients[common.CoreDataClientName].Url() + clients.ApiValueDescriptorRoute)) + local.New(config.Clients[CoreDataClientName].Url() + clients.ApiValueDescriptorRoute)) } - if _, ok := config.Clients[common.CoreCommandClientName]; ok { + if _, ok := config.Clients[CoreCommandClientName]; ok { commandClient = command.NewCommandClient( - local.New(config.Clients[common.CoreCommandClientName].Url() + clients.ApiDeviceRoute)) + local.New(config.Clients[CoreCommandClientName].Url() + clients.ApiDeviceRoute)) } - if _, ok := config.Clients[common.NotificationsClientName]; ok { + if _, ok := config.Clients[NotificationsClientName]; ok { notificationsClient = notifications.NewNotificationsClient( - local.New(config.Clients[common.NotificationsClientName].Url() + clients.ApiNotificationRoute)) + local.New(config.Clients[NotificationsClientName].Url() + clients.ApiNotificationRoute)) } // Note that all the clients are optional so some or all these clients may be nil diff --git a/internal/bootstrap/handlers/clients_test.go b/internal/bootstrap/handlers/clients_test.go index f04f5c2f9..bad02a3b3 100644 --- a/internal/bootstrap/handlers/clients_test.go +++ b/internal/bootstrap/handlers/clients_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,13 +24,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" ) @@ -102,15 +102,15 @@ func TestClientsBootstrapHandler(t *testing.T) { configuration.Clients = make(map[string]config.ClientInfo) if test.CoreDataClientInfo != nil { - configuration.Clients[common.CoreDataClientName] = coreDataClientInfo + configuration.Clients[CoreDataClientName] = coreDataClientInfo } if test.CommandClientInfo != nil { - configuration.Clients[common.CoreCommandClientName] = commandClientInfo + configuration.Clients[CoreCommandClientName] = commandClientInfo } if test.NotificationsClientInfo != nil { - configuration.Clients[common.NotificationsClientName] = notificationsClientInfo + configuration.Clients[NotificationsClientName] = notificationsClientInfo } dic.Update(di.ServiceConstructorMap{ diff --git a/internal/bootstrap/handlers/storeclient.go b/internal/bootstrap/handlers/storeclient.go index 512a83cc1..dab9614e8 100644 --- a/internal/bootstrap/handlers/storeclient.go +++ b/internal/bootstrap/handlers/storeclient.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ func NewDatabase() *Database { // BootstrapHandler creates the new interfaces.StoreClient use for database access by Store & Forward capability func (_ *Database) BootstrapHandler( - ctx context.Context, + _ context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { @@ -62,12 +62,12 @@ func (_ *Database) BootstrapHandler( return true } - logger := bootstrapContainer.LoggingClientFrom(dic.Get) + lc := bootstrapContainer.LoggingClientFrom(dic.Get) secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get) - storeClient, err := InitializeStoreClient(secretProvider, config, startupTimer, logger) + storeClient, err := InitializeStoreClient(secretProvider, config, startupTimer, lc) if err != nil { - logger.Error(err.Error()) + lc.Error(err.Error()) return false } diff --git a/internal/bootstrap/handlers/telemetry.go b/internal/bootstrap/handlers/telemetry.go index 9abc8983c..a9a63824d 100644 --- a/internal/bootstrap/handlers/telemetry.go +++ b/internal/bootstrap/handlers/telemetry.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ func NewTelemetry() *Telemetry { func (_ *Telemetry) BootstrapHandler( ctx context.Context, wg *sync.WaitGroup, - startupTimer startup.Timer, + _ startup.Timer, dic *di.Container) bool { logger := container.LoggingClientFrom(dic.Get) diff --git a/internal/bootstrap/handlers/version.go b/internal/bootstrap/handlers/version.go index cc7c59207..0cda1c5be 100644 --- a/internal/bootstrap/handlers/version.go +++ b/internal/bootstrap/handlers/version.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) const ( @@ -85,7 +84,7 @@ func (vv *VersionValidator) BootstrapHandler( return true } - url := config.Clients[common.CoreDataClientName].Url() + clients.ApiVersionRoute + url := config.Clients[CoreDataClientName].Url() + clients.ApiVersionRoute var data []byte var err error for startupTimer.HasNotElapsed() { diff --git a/internal/bootstrap/handlers/version_test.go b/internal/bootstrap/handlers/version_test.go index a1fe2dea5..ee3704865 100644 --- a/internal/bootstrap/handlers/version_test.go +++ b/internal/bootstrap/handlers/version_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ func TestValidateVersionMatch(t *testing.T) { startupTimer := startup.NewStartUpTimer("unit-test") clients := make(map[string]config.ClientInfo) - clients[common.CoreDataClientName] = config.ClientInfo{ + clients[CoreDataClientName] = config.ClientInfo{ Protocol: "http", Host: "localhost", Port: 0, // Will be replaced by local test webserver's port @@ -115,9 +115,9 @@ func TestValidateVersionMatch(t *testing.T) { testServerUrl, _ := url.Parse(testServer.URL) port, _ := strconv.Atoi(testServerUrl.Port()) - coreService := configuration.Clients[common.CoreDataClientName] + coreService := configuration.Clients[CoreDataClientName] coreService.Port = port - configuration.Clients[common.CoreDataClientName] = coreService + configuration.Clients[CoreDataClientName] = coreService validator := NewVersionValidator(test.skipVersionCheck, test.SdkVersion) result := validator.BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) diff --git a/internal/common/clients.go b/internal/common/clients.go deleted file mode 100644 index 64baa1902..000000000 --- a/internal/common/clients.go +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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 common - -import ( - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" -) - -const ( - CoreCommandClientName = "Command" - CoreDataClientName = "CoreData" - NotificationsClientName = "Notifications" -) - -type EdgeXClients struct { - LoggingClient logger.LoggingClient - EventClient coredata.EventClient - CommandClient command.CommandClient - ValueDescriptorClient coredata.ValueDescriptorClient - NotificationsClient notifications.NotificationsClient -} diff --git a/internal/constants.go b/internal/constants.go index 7e800dd9a..08b1ed9a3 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,7 +22,6 @@ import ( const ( ConfigRegistryStem = "edgex/appservices/1.0/" - DatabaseName = "application-service" CorrelationHeaderKey = "X-Correlation-ID" ApiTriggerRoute = contracts.ApiBase + "/trigger" @@ -30,7 +29,7 @@ const ( ) // SDKVersion indicates the version of the SDK - will be overwritten by build -var SDKVersion string = "0.0.0" +var SDKVersion = "0.0.0" // ApplicationVersion indicates the version of the application itself, not the SDK - will be overwritten by build -var ApplicationVersion string = "0.0.0" +var ApplicationVersion = "0.0.0" diff --git a/internal/controller/rest/controller.go b/internal/controller/rest/controller.go index 272180ec6..66ea1e2a5 100644 --- a/internal/controller/rest/controller.go +++ b/internal/controller/rest/controller.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,7 +22,11 @@ import ( "net/http" "strings" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/telemetry" @@ -46,16 +50,12 @@ type Controller struct { } // NewController creates and initializes an Controller -func NewController( - router *mux.Router, - lc logger.LoggingClient, - config *sdkCommon.ConfigurationStruct, - secretProvider interfaces.SecretProvider) *Controller { +func NewController(router *mux.Router, dic *di.Container) *Controller { return &Controller{ router: router, - secretProvider: secretProvider, - lc: lc, - config: config, + secretProvider: bootstrapContainer.SecretProviderFrom(dic.Get), + lc: bootstrapContainer.LoggingClientFrom(dic.Get), + config: container.ConfigurationFrom(dic.Get), } } diff --git a/internal/controller/rest/controller_test.go b/internal/controller/rest/controller_test.go index a867ac5a7..740341709 100644 --- a/internal/controller/rest/controller_test.go +++ b/internal/controller/rest/controller_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 202` 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,7 +27,11 @@ import ( "testing" "time" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" @@ -45,9 +49,18 @@ import ( ) var expectedCorrelationId = uuid.New().String() +var dic *di.Container + +func TestMain(m *testing.M) { + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} func TestPingRequest(t *testing.T) { - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiPingRoute, target.Ping, nil) @@ -68,7 +81,7 @@ func TestVersionRequest(t *testing.T) { internal.ApplicationVersion = expectedAppVersion internal.SDKVersion = expectedSdkVersion - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiVersion, target.Version, nil) @@ -82,7 +95,7 @@ func TestVersionRequest(t *testing.T) { } func TestMetricsRequest(t *testing.T) { - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiMetricsRoute, target.Metrics, nil) @@ -112,7 +125,13 @@ func TestConfigRequest(t *testing.T) { }, } - target := NewController(nil, logger.NewMockClient(), &expectedConfig, nil) + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &expectedConfig + }, + }) + + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiConfigRoute, target.Config, nil) @@ -136,15 +155,18 @@ func TestConfigRequest(t *testing.T) { func TestAddSecretRequest(t *testing.T) { expectedRequestId := "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" - config := &sdkCommon.ConfigurationStruct{} - lc := logger.NewMockClient() + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &sdkCommon.ConfigurationStruct{} + }, + }) mockProvider := &mocks.SecretProvider{} mockProvider.On("StoreSecrets", "/mqtt", map[string]string{"password": "password", "username": "username"}).Return(nil) mockProvider.On("StoreSecrets", "/no", map[string]string{"password": "password", "username": "username"}).Return(errors.New("Invalid w/o Vault")) - target := NewController(nil, lc, config, mockProvider) + target := NewController(nil, dic) assert.NotNil(t, target) validRequest := common.SecretRequest{ diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 8f874bd9b..b749a2935 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,13 +26,13 @@ import ( "strings" "sync" - edgexErrors "github.com/edgexfoundry/go-mod-core-contracts/v2/errors" - "github.com/google/uuid" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + edgexErrors "github.com/edgexfoundry/go-mod-core-contracts/v2/errors" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" @@ -40,21 +40,18 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/requests" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - dbInterfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" - "github.com/fxamacker/cbor/v2" + "github.com/google/uuid" ) // GolangRuntime represents the golang runtime environment type GolangRuntime struct { - TargetType interface{} - ServiceKey string - transforms []appcontext.AppFunction - isBusyCopying sync.Mutex - storeForward storeForwardInfo - secretProvider interfaces.SecretProvider + TargetType interface{} + ServiceKey string + transforms []interfaces.AppFunction + isBusyCopying sync.Mutex + storeForward storeForwardInfo + dic *di.Container } type MessageError struct { @@ -63,14 +60,14 @@ type MessageError struct { } // Initialize sets the internal reference to the StoreClient for use when Store and Forward is enabled -func (gr *GolangRuntime) Initialize(storeClient dbInterfaces.StoreClient, secretProvider interfaces.SecretProvider) { - gr.storeForward.storeClient = storeClient +func (gr *GolangRuntime) Initialize(dic *di.Container) { + gr.dic = dic gr.storeForward.runtime = gr - gr.secretProvider = secretProvider + gr.storeForward.dic = dic } // SetTransforms is thread safe to set transforms -func (gr *GolangRuntime) SetTransforms(transforms []appcontext.AppFunction) { +func (gr *GolangRuntime) SetTransforms(transforms []interfaces.AppFunction) { gr.isBusyCopying.Lock() gr.transforms = transforms gr.storeForward.pipelineHash = gr.storeForward.calculatePipelineHash() // Only need to calculate hash when the pipeline changes. @@ -78,8 +75,8 @@ func (gr *GolangRuntime) SetTransforms(transforms []appcontext.AppFunction) { } // ProcessMessage sends the contents of the message thru the functions pipeline -func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelope types.MessageEnvelope) *MessageError { - lc := edgexcontext.LoggingClient +func (gr *GolangRuntime) ProcessMessage(appContext *appfunction.Context, envelope types.MessageEnvelope) *MessageError { + lc := appContext.LoggingClient() if len(gr.transforms) == 0 { err := errors.New("No transforms configured. Please check log for errors loading pipeline") @@ -145,7 +142,7 @@ func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelo } } - edgexcontext.CorrelationID = envelope.CorrelationID + appContext.SetCorrelationID(envelope.CorrelationID) // All functions expect an object, not a pointer to an object, so must use reflection to // dereference to pointer to the object @@ -153,42 +150,46 @@ func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelo // Make copy of transform functions to avoid disruption of pipeline when updating the pipeline from registry gr.isBusyCopying.Lock() - transforms := make([]appcontext.AppFunction, len(gr.transforms)) + transforms := make([]interfaces.AppFunction, len(gr.transforms)) copy(transforms, gr.transforms) gr.isBusyCopying.Unlock() - return gr.ExecutePipeline(target, envelope.ContentType, edgexcontext, transforms, 0, false) + return gr.ExecutePipeline(target, envelope.ContentType, appContext, transforms, 0, false) } -func (gr *GolangRuntime) ExecutePipeline(target interface{}, contentType string, edgexcontext *appcontext.Context, - transforms []appcontext.AppFunction, startPosition int, isRetry bool) *MessageError { +func (gr *GolangRuntime) ExecutePipeline( + target interface{}, + contentType string, + appContext *appfunction.Context, + transforms []interfaces.AppFunction, + startPosition int, + isRetry bool) *MessageError { var result interface{} var continuePipeline = true - edgexcontext.SecretProvider = gr.secretProvider - for functionIndex, trxFunc := range transforms { if functionIndex < startPosition { continue } - edgexcontext.RetryData = nil + appContext.SetRetryData(nil) if result == nil { - continuePipeline, result = trxFunc(edgexcontext, target, contentType) + appContext.SetInputContentType(contentType) + continuePipeline, result = trxFunc(appContext, target) } else { - continuePipeline, result = trxFunc(edgexcontext, result) + continuePipeline, result = trxFunc(appContext, result) } if continuePipeline != true { if result != nil { if err, ok := result.(error); ok { - edgexcontext.LoggingClient.Error( + appContext.LoggingClient().Error( fmt.Sprintf("Pipeline function #%d resulted in error", functionIndex), - "error", err.Error(), clients.CorrelationHeader, edgexcontext.CorrelationID) - if edgexcontext.RetryData != nil && !isRetry { - gr.storeForward.storeForLaterRetry(edgexcontext.RetryData, edgexcontext, functionIndex) + "error", err.Error(), clients.CorrelationHeader, appContext.CorrelationID) + if appContext.RetryData() != nil && !isRetry { + gr.storeForward.storeForLaterRetry(appContext.RetryData(), appContext, functionIndex) } return &MessageError{Err: err, ErrorCode: http.StatusUnprocessableEntity} @@ -206,11 +207,9 @@ func (gr *GolangRuntime) StartStoreAndForward( appCtx context.Context, enabledWg *sync.WaitGroup, enabledCtx context.Context, - serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { + serviceKey string) { - gr.storeForward.startStoreAndForwardRetryLoop(appWg, appCtx, enabledWg, enabledCtx, serviceKey, config, edgeXClients) + gr.storeForward.startStoreAndForwardRetryLoop(appWg, appCtx, enabledWg, enabledCtx, serviceKey) } func (gr *GolangRuntime) processEventPayload(envelope types.MessageEnvelope, lc logger.LoggingClient) (*dtos.Event, error) { diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 8a8f8d18e..ba1fc3374 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,13 +21,15 @@ import ( "net/http" "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" @@ -39,8 +41,6 @@ import ( "github.com/stretchr/testify/require" ) -var lc logger.LoggingClient - const ( serviceKey = "AppService-UnitTest" ) @@ -50,7 +50,7 @@ var testV2Event = testAddEventRequest.Event func createAddEventRequest() requests.AddEventRequest { event := dtos.NewEvent("Thermostat", "FamilyRoomThermostat", "Temperature") - event.AddSimpleReading("Temperature", v2.ValueTypeInt64, int64(72)) + _ = event.AddSimpleReading("Temperature", v2.ValueTypeInt64, int64(72)) request := requests.NewAddEventRequest(event) return request } @@ -74,11 +74,7 @@ var testV1Event = models.Event{ Tags: nil, } -func init() { - lc = logger.NewMockClient() -} - -func TestProcessMessageBasRequest(t *testing.T) { +func TestProcessMessageBusRequest(t *testing.T) { expected := http.StatusBadRequest badRequest := testAddEventRequest @@ -92,17 +88,15 @@ func TestProcessMessageBasRequest(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") - dummyTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + dummyTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{dummyTransform}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{dummyTransform}) result := runtime.ProcessMessage(context, envelope) require.NotNil(t, result) assert.Equal(t, expected, result.ErrorCode) @@ -118,12 +112,10 @@ func TestProcessMessageNoTransforms(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") runtime := GolangRuntime{} - runtime.Initialize(nil, nil) + runtime.Initialize(nil) result := runtime.ProcessMessage(context, envelope) require.NotNil(t, result) @@ -139,13 +131,12 @@ func TestProcessMessageOneCustomTransform(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + transform1WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") - if result, ok := params[0].(*dtos.Event); ok { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + require.NotNil(t, data, "should have been passed the first event from CoreData") + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") require.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event") } @@ -153,8 +144,8 @@ func TestProcessMessageOneCustomTransform(t *testing.T) { return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) require.True(t, transform1WasCalled, "transform1 should have been called") @@ -169,32 +160,30 @@ func TestProcessMessageTwoCustomTransforms(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") transform1WasCalled := false transform2WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") - if result, ok := params[0].(dtos.Event); ok { + require.NotNil(t, data, "should have been passed the first event from CoreData") + if result, ok := data.(dtos.Event); ok { require.True(t, ok, "Should have received Event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected Event") } return true, "Transform1Result" } - transform2 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform2 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform2WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1, transform2}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1, transform2}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) @@ -211,37 +200,36 @@ func TestProcessMessageThreeCustomTransformsOneFail(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + transform1WasCalled := false transform2WasCalled := false transform3WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") + require.NotNil(t, data, "should have been passed the first event from CoreData") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") require.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event") } return false, "Transform1Result" } - transform2 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform2 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform2WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } - transform3 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform3 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform3WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1, transform2, transform3}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1, transform2, transform3}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) @@ -265,14 +253,13 @@ func TestProcessMessageTransformError(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + // Let the Runtime know we are sending a RegistryInfo so it passes it to the first function runtime := GolangRuntime{TargetType: &config.RegistryInfo{}} - runtime.Initialize(nil, nil) + runtime.Initialize(nil) // FilterByDeviceName with return an error if it doesn't receive and Event - runtime.SetTransforms([]appcontext.AppFunction{transforms.NewFilterFor([]string{"SomeDevice"}).FilterByDeviceName}) + runtime.SetTransforms([]interfaces.AppFunction{transforms.NewFilterFor([]string{"SomeDevice"}).FilterByDeviceName}) err := runtime.ProcessMessage(context, envelope) require.NotNil(t, err, "Expected an error") @@ -295,17 +282,14 @@ func TestProcessMessageJSON(t *testing.T) { ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext("testing", dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -315,8 +299,8 @@ func TestProcessMessageJSON(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nilf(t, result, "result should be null. Got %v", result) @@ -337,17 +321,14 @@ func TestProcessMessageCBOR(t *testing.T) { ContentType: clients.ContentTypeCBOR, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext("testing", dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -357,8 +338,8 @@ func TestProcessMessageCBOR(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nil(t, result, "result should be null") @@ -369,7 +350,7 @@ type CustomType struct { ID string `json:"id"` } -// Must implement the Marshaller interface so SetOutputData will marshal it to JSON +// Must implement the Marshaller interface so SetResponseData will marshal it to JSON func (custom CustomType) MarshalJSON() ([]byte, error) { test := struct { ID string `json:"id"` @@ -424,13 +405,11 @@ func TestProcessMessageTargetType(t *testing.T) { ContentType: currentTest.ContentType, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testing", dic, "") runtime := GolangRuntime{TargetType: currentTest.TargetType} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transforms.NewOutputData().SetOutputData}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transforms.NewResponseData().SetResponseData}) err := runtime.ProcessMessage(context, envelope) if currentTest.ErrorExpected { @@ -440,49 +419,36 @@ func TestProcessMessageTargetType(t *testing.T) { assert.Nil(t, err, fmt.Sprintf("unexpected error for test '%s'", currentTest.Name)) } - // OutputData will be nil if an error occurred in the pipeline processing the data - assert.Equal(t, currentTest.ExpectedOutputData, context.OutputData, fmt.Sprintf("'%s' test failed", currentTest.Name)) + // ResponseData will be nil if an error occurred in the pipeline processing the data + assert.Equal(t, currentTest.ExpectedOutputData, context.ResponseData(), fmt.Sprintf("'%s' test failed", currentTest.Name)) }) } } func TestExecutePipelinePersist(t *testing.T) { expectedItemCount := 1 - configuration := common.ConfigurationStruct{ - Writable: common.WritableInfo{ - LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{ - Enabled: true, - MaxRetryCount: 10}, - }, - } - - ctx := appcontext.Context{ - Configuration: &configuration, - LoggingClient: lc, - CorrelationID: "CorrelationID", - } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + context := appfunction.NewContext("testing", dic, "") + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } runtime := GolangRuntime{ServiceKey: serviceKey} - runtime.Initialize(creatMockStoreClient(), nil) + runtime.Initialize(updateDicWithMockStoreClient()) httpPost := transforms.NewHTTPSender("http://nowhere", "", true).HTTPPost - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, httpPost}) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, httpPost}) payload := []byte("My Payload") // Target of this test - actual := runtime.ExecutePipeline(payload, "", &ctx, runtime.transforms, 0, false) + actual := runtime.ExecutePipeline(payload, "", context, runtime.transforms, 0, false) require.NotNil(t, actual) require.Error(t, actual.Err, "Error expected from export function") storedObjects := mockRetrieveObjects(serviceKey) require.Equal(t, expectedItemCount, len(storedObjects), "unexpected item count") assert.Equal(t, serviceKey, storedObjects[0].AppServiceKey, "AppServiceKey not as expected") - assert.Equal(t, ctx.CorrelationID, storedObjects[0].CorrelationID, "CorrelationID not as expected") + assert.Equal(t, context.CorrelationID(), storedObjects[0].CorrelationID, "CorrelationID not as expected") } // TODO: Remove once switch completely to V2 Event DTOs @@ -498,17 +464,14 @@ func TestProcessMessageJSONWithV1Event(t *testing.T) { ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext(expectedCorrelationID, dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -518,8 +481,8 @@ func TestProcessMessageJSONWithV1Event(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nil(t, result, "result should be null") @@ -573,7 +536,7 @@ func TestGolangRuntime_processEventPayload(t *testing.T) { envelope.Payload = testCase.Payload envelope.ContentType = testCase.ContentType - actual, err := target.processEventPayload(envelope, lc) + actual, err := target.processEventPayload(envelope, logger.NewMockClient()) if testCase.ExpectError { require.Error(t, err) return @@ -611,7 +574,7 @@ func TestGolangRuntime_unmarshalV1EventToV2Event(t *testing.T) { envelope.Payload = testCase.Payload envelope.ContentType = testCase.ContentType - actual, err := target.unmarshalV1EventToV2Event(envelope, lc) + actual, err := target.unmarshalV1EventToV2Event(envelope, logger.NewMockClient()) require.NoError(t, err) require.Equal(t, expectedEvent, *actual) }) diff --git a/internal/runtime/storeforward.go b/internal/runtime/storeforward.go index 352af89c2..39e54b2e8 100644 --- a/internal/runtime/storeforward.go +++ b/internal/runtime/storeforward.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,20 +24,23 @@ import ( "sync" "time" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/contracts" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" ) const ( - defaultMinRetryInterval = time.Duration(1 * time.Second) + defaultMinRetryInterval = 1 * time.Second ) type storeForwardInfo struct { runtime *GolangRuntime - storeClient interfaces.StoreClient + dic *di.Container pipelineHash string } @@ -46,37 +49,38 @@ func (sf *storeForwardInfo) startStoreAndForwardRetryLoop( appCtx context.Context, enabledWg *sync.WaitGroup, enabledCtx context.Context, - serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { + serviceKey string) { appWg.Add(1) enabledWg.Add(1) + config := container.ConfigurationFrom(sf.dic.Get) + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + go func() { defer appWg.Done() defer enabledWg.Done() retryInterval, err := time.ParseDuration(config.Writable.StoreAndForward.RetryInterval) if err != nil { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward RetryInterval failed to parse, defaulting to %s", defaultMinRetryInterval.String())) retryInterval = defaultMinRetryInterval } else if retryInterval < defaultMinRetryInterval { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward RetryInterval value %s is less than the allowed minimum value, defaulting to %s", retryInterval.String(), defaultMinRetryInterval.String())) retryInterval = defaultMinRetryInterval } if config.Writable.StoreAndForward.MaxRetryCount < 0 { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward MaxRetryCount can not be less than 0, defaulting to 1")) config.Writable.StoreAndForward.MaxRetryCount = 1 } - edgeXClients.LoggingClient.Info( + lc.Info( fmt.Sprintf("Starting StoreAndForward Retry Loop with %s RetryInterval and %d max retries", retryInterval.String(), config.Writable.StoreAndForward.MaxRetryCount)) @@ -93,61 +97,66 @@ func (sf *storeForwardInfo) startStoreAndForwardRetryLoop( break exit case <-time.After(retryInterval): - sf.retryStoredData(serviceKey, config, edgeXClients) + sf.retryStoredData(serviceKey) } } - edgeXClients.LoggingClient.Info("Exiting StoreAndForward Retry Loop") + lc.Info("Exiting StoreAndForward Retry Loop") }() } -func (sf *storeForwardInfo) storeForLaterRetry(payload []byte, - edgexcontext *appcontext.Context, +func (sf *storeForwardInfo) storeForLaterRetry( + payload []byte, + appContext interfaces.AppFunctionContext, pipelinePosition int) { item := contracts.NewStoredObject(sf.runtime.ServiceKey, payload, pipelinePosition, sf.pipelineHash) - item.CorrelationID = edgexcontext.CorrelationID + item.CorrelationID = appContext.CorrelationID() - edgexcontext.LoggingClient.Trace("Storing data for later retry", - clients.CorrelationHeader, edgexcontext.CorrelationID) + appContext.LoggingClient().Trace("Storing data for later retry", + clients.CorrelationHeader, appContext.CorrelationID) - if !edgexcontext.Configuration.Writable.StoreAndForward.Enabled { - edgexcontext.LoggingClient.Error( + config := container.ConfigurationFrom(sf.dic.Get) + if !config.Writable.StoreAndForward.Enabled { + appContext.LoggingClient().Error( "Failed to store item for later retry", "error", "StoreAndForward not enabled", clients.CorrelationHeader, item.CorrelationID) return } - if _, err := sf.storeClient.Store(item); err != nil { - edgexcontext.LoggingClient.Error("Failed to store item for later retry", + storeClient := container.StoreClientFrom(sf.dic.Get) + + if _, err := storeClient.Store(item); err != nil { + appContext.LoggingClient().Error("Failed to store item for later retry", "error", err, clients.CorrelationHeader, item.CorrelationID) } } -func (sf *storeForwardInfo) retryStoredData(serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { +func (sf *storeForwardInfo) retryStoredData(serviceKey string) { - items, err := sf.storeClient.RetrieveFromStore(serviceKey) + storeClient := container.StoreClientFrom(sf.dic.Get) + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + + items, err := storeClient.RetrieveFromStore(serviceKey) if err != nil { - edgeXClients.LoggingClient.Error("Unable to load store and forward items from DB", "error", err) + lc.Error("Unable to load store and forward items from DB", "error", err) return } - edgeXClients.LoggingClient.Debug(fmt.Sprintf(" %d stored data items found for retrying", len(items))) + lc.Debug(fmt.Sprintf(" %d stored data items found for retrying", len(items))) if len(items) > 0 { - itemsToRemove, itemsToUpdate := sf.processRetryItems(items, config, edgeXClients) + itemsToRemove, itemsToUpdate := sf.processRetryItems(items) - edgeXClients.LoggingClient.Debug( + lc.Debug( fmt.Sprintf(" %d stored data items will be removed post retry", len(itemsToRemove))) - edgeXClients.LoggingClient.Debug( + lc.Debug( fmt.Sprintf(" %d stored data items will be update post retry", len(itemsToUpdate))) for _, item := range itemsToRemove { - if err := sf.storeClient.RemoveFromStore(item); err != nil { - edgeXClients.LoggingClient.Error( + if err := storeClient.RemoveFromStore(item); err != nil { + lc.Error( "Unable to remove stored data item from DB", "error", err, "objectID", item.ID, @@ -156,8 +165,8 @@ func (sf *storeForwardInfo) retryStoredData(serviceKey string, } for _, item := range itemsToUpdate { - if err := sf.storeClient.Update(item); err != nil { - edgeXClients.LoggingClient.Error("Unable to update stored data item in DB", + if err := storeClient.Update(item); err != nil { + lc.Error("Unable to update stored data item in DB", "error", err, "objectID", item.ID, clients.CorrelationHeader, item.CorrelationID) @@ -166,20 +175,20 @@ func (sf *storeForwardInfo) retryStoredData(serviceKey string, } } -func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) ([]contracts.StoredObject, []contracts.StoredObject) { +func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject) ([]contracts.StoredObject, []contracts.StoredObject) { + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + config := container.ConfigurationFrom(sf.dic.Get) var itemsToRemove []contracts.StoredObject var itemsToUpdate []contracts.StoredObject for _, item := range items { if item.Version == sf.calculatePipelineHash() { - if !sf.retryExportFunction(item, config, edgeXClients) { + if !sf.retryExportFunction(item) { item.RetryCount++ if config.Writable.StoreAndForward.MaxRetryCount == 0 || item.RetryCount < config.Writable.StoreAndForward.MaxRetryCount { - edgeXClients.LoggingClient.Trace("Export retry failed. Incrementing retry count", + lc.Trace("Export retry failed. Incrementing retry count", "retries", item.RetryCount, clients.CorrelationHeader, @@ -188,20 +197,20 @@ func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, continue } - edgeXClients.LoggingClient.Trace( + lc.Trace( "Max retries exceeded. Removing item from DB", "retries", item.RetryCount, clients.CorrelationHeader, item.CorrelationID) // Note that item will be removed for DB below. } else { - edgeXClients.LoggingClient.Trace( + lc.Trace( "Export retry successful. Removing item from DB", clients.CorrelationHeader, item.CorrelationID) } } else { - edgeXClients.LoggingClient.Error( + lc.Error( "Stored data item's Function Pipeline Version doesn't match current Function Pipeline Version. Removing item from DB", clients.CorrelationHeader, item.CorrelationID) @@ -218,24 +227,15 @@ func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, return itemsToRemove, itemsToUpdate } -func (sf *storeForwardInfo) retryExportFunction(item contracts.StoredObject, config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) bool { - edgexContext := &appcontext.Context{ - CorrelationID: item.CorrelationID, - Configuration: config, - LoggingClient: edgeXClients.LoggingClient, - EventClient: edgeXClients.EventClient, - ValueDescriptorClient: edgeXClients.ValueDescriptorClient, - CommandClient: edgeXClients.CommandClient, - NotificationsClient: edgeXClients.NotificationsClient, - } +func (sf *storeForwardInfo) retryExportFunction(item contracts.StoredObject) bool { + appContext := appfunction.NewContext(item.CorrelationID, sf.dic, "") - edgexContext.LoggingClient.Trace("Retrying stored data", clients.CorrelationHeader, edgexContext.CorrelationID) + appContext.LoggingClient().Trace("Retrying stored data", clients.CorrelationHeader, appContext.CorrelationID) return sf.runtime.ExecutePipeline( item.Payload, "", - edgexContext, + appContext, sf.runtime.transforms, item.PipelinePosition, true) == nil diff --git a/internal/runtime/storeforward_test.go b/internal/runtime/storeforward_test.go index 0b67ea586..b2f7e0573 100644 --- a/internal/runtime/storeforward_test.go +++ b/internal/runtime/storeforward_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,42 +18,61 @@ package runtime import ( "errors" + "os" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/stretchr/testify/require" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/contracts" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces/mocks" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" ) -func TestProcessRetryItems(t *testing.T) { - - targetTransformWasCalled := false - expectedPayload := "This is a sample payload" +var dic *di.Container +func TestMain(m *testing.M) { config := common.ConfigurationStruct{ Writable: common.WritableInfo{ LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{MaxRetryCount: 10}, + StoreAndForward: common.StoreAndForwardInfo{Enabled: true, MaxRetryCount: 10}, }, } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) + + os.Exit(m.Run()) +} + +func TestProcessRetryItems(t *testing.T) { + + targetTransformWasCalled := false + expectedPayload := "This is a sample payload" + + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } - successTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + successTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { targetTransformWasCalled = true - actualPayload, ok := params[0].([]byte) + actualPayload, ok := data.([]byte) require.True(t, ok, "Expected []byte payload") require.Equal(t, expectedPayload, string(actualPayload)) @@ -61,16 +80,15 @@ func TestProcessRetryItems(t *testing.T) { return false, nil } - failureTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + failureTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { targetTransformWasCalled = true return false, errors.New("I failed") } - runtime := GolangRuntime{} tests := []struct { Name string - TargetTransform appcontext.AppFunction + TargetTransform interfaces.AppFunction TargetTransformWasCalled bool ExpectedPayload string RetryCount int @@ -88,8 +106,8 @@ func TestProcessRetryItems(t *testing.T) { t.Run(test.Name, func(t *testing.T) { targetTransformWasCalled = false - runtime.Initialize(creatMockStoreClient(), nil) - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, transformPassthru, test.TargetTransform}) + runtime.Initialize(dic) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, transformPassthru, test.TargetTransform}) version := runtime.storeForward.pipelineHash if test.BadVersion { @@ -98,7 +116,7 @@ func TestProcessRetryItems(t *testing.T) { storedObject := contracts.NewStoredObject("dummy", []byte(test.ExpectedPayload), 2, version) storedObject.RetryCount = test.RetryCount - removes, updates := runtime.storeForward.processRetryItems([]contracts.StoredObject{storedObject}, &config, common.EdgeXClients{LoggingClient: lc}) + removes, updates := runtime.storeForward.processRetryItems([]contracts.StoredObject{storedObject}) assert.Equal(t, test.TargetTransformWasCalled, targetTransformWasCalled, "Target transform not called") if test.RetryCount != test.ExpectedRetryCount { if assert.True(t, len(updates) > 0, "Remove count not as expected") { @@ -113,23 +131,18 @@ func TestProcessRetryItems(t *testing.T) { func TestDoStoreAndForwardRetry(t *testing.T) { serviceKey := "AppService-UnitTest" payload := []byte("My Payload") - config := common.ConfigurationStruct{ - Writable: common.WritableInfo{ - LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{MaxRetryCount: 10}}, - } httpPost := transforms.NewHTTPSender("http://nowhere", "", true).HTTPPost - successTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + successTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return false, nil } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } tests := []struct { Name string - TargetTransform appcontext.AppFunction + TargetTransform interfaces.AppFunction RetryCount int ExpectedRetryCount int ExpectedObjectCount int @@ -142,8 +155,8 @@ func TestDoStoreAndForwardRetry(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { runtime := GolangRuntime{ServiceKey: serviceKey} - runtime.Initialize(creatMockStoreClient(), nil) - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, test.TargetTransform}) + runtime.Initialize(updateDicWithMockStoreClient()) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, test.TargetTransform}) object := contracts.NewStoredObject(serviceKey, payload, 1, runtime.storeForward.calculatePipelineHash()) object.CorrelationID = "CorrelationID" @@ -152,7 +165,7 @@ func TestDoStoreAndForwardRetry(t *testing.T) { _, _ = mockStoreObject(object) // Target of this test - runtime.storeForward.retryStoredData(serviceKey, &config, common.EdgeXClients{LoggingClient: lc}) + runtime.storeForward.retryStoredData(serviceKey) objects := mockRetrieveObjects(serviceKey) if assert.Equal(t, test.ExpectedObjectCount, len(objects)) && test.ExpectedObjectCount > 0 { @@ -166,7 +179,7 @@ func TestDoStoreAndForwardRetry(t *testing.T) { var mockObjectStore map[string]contracts.StoredObject -func creatMockStoreClient() interfaces.StoreClient { +func updateDicWithMockStoreClient() *di.Container { mockObjectStore = make(map[string]contracts.StoredObject) storeClient := &mocks.StoreClient{} storeClient.Mock.On("Store", mock.Anything).Return(mockStoreObject) @@ -174,7 +187,13 @@ func creatMockStoreClient() interfaces.StoreClient { storeClient.Mock.On("Update", mock.Anything).Return(mockUpdateObject) storeClient.Mock.On("RetrieveFromStore", mock.Anything).Return(mockRetrieveObjects, nil) - return storeClient + dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return storeClient + }, + }) + + return dic } func mockStoreObject(object contracts.StoredObject) (string, error) { diff --git a/internal/store/db/db.go b/internal/store/db/db.go index 211bfbd33..b5f661051 100644 --- a/internal/store/db/db.go +++ b/internal/store/db/db.go @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. + * Copyright (c) 2021 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 @@ -19,7 +20,6 @@ import "errors" const ( // Database providers - MongoDB = "mongodb" RedisDB = "redisdb" ) diff --git a/internal/store/db/redis/store.go b/internal/store/db/redis/store.go index 422ef1be0..89dda1237 100644 --- a/internal/store/db/redis/store.go +++ b/internal/store/db/redis/store.go @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. + * Copyright (c) 2021 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,6 +26,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/redis/models" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/gomodule/redigo/redis" @@ -43,7 +45,7 @@ type Client struct { // Store persists a stored object to the data store. Three ("Three shall be the number thou shalt // count, and the number of the counting shall be three") keys are used: -// * the object id to point to a STRING which is the marshal'ed JSON. +// * the object id to point to a STRING which is the marshalled JSON. // * the object AppServiceKey to point to a SET containing all object ids associated with this // app service. Note the key is prefixed to avoid key collisions. // * the object id to point to a HASH which contains the object AppServiceKey. @@ -54,7 +56,7 @@ func (c Client) Store(o contracts.StoredObject) (string, error) { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() exists, err := redis.Bool(conn.Do("EXISTS", o.ID)) if err != nil { @@ -95,7 +97,7 @@ func (c Client) RetrieveFromStore(appServiceKey string) (objects []contracts.Sto } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() ids, err := redis.Values(conn.Do("SMEMBERS", nameSpace+":idl:"+appServiceKey)) if err != nil { @@ -132,7 +134,7 @@ func (c Client) Update(o contracts.StoredObject) error { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() // retrieve the current AppServiceKey for this store object currentASK, err := redis.String(conn.Do("HGET", nameSpace+":ask:"+o.ID, "ASK")) @@ -174,7 +176,7 @@ func (c Client) RemoveFromStore(o contracts.StoredObject) error { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() _ = conn.Send("MULTI") // remove the object's representation diff --git a/internal/store/factory.go b/internal/store/factory.go index 456c15a76..9708fe454 100644 --- a/internal/store/factory.go +++ b/internal/store/factory.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2020 Intel Inc. + * Copyright 2021 Intel Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -19,6 +19,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/redis" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" ) diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 8f58f460f..3cbdbec9b 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2019 Dell Inc., Intel Corporation + * Copyright 2021 Dell Inc., 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 @@ -46,7 +46,6 @@ type CpuUsage struct { Total uint64 // reported sum total of all usage } -var once sync.Once var lastSample CpuUsage var usageAvg float64 diff --git a/internal/telemetry/windows_cpu.go b/internal/telemetry/windows_cpu.go index 16c2d0254..ae07e1f55 100644 --- a/internal/telemetry/windows_cpu.go +++ b/internal/telemetry/windows_cpu.go @@ -1,7 +1,7 @@ // +build windows // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ import ( ) var ( - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetSystemTimes = modkernel32.NewProc("GetSystemTimes") + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetSystemTimes = modKernel32.NewProc("GetSystemTimes") ) func PollCpu() (cpuSnapshot CpuUsage) { @@ -69,7 +69,7 @@ func getSystemTimes(idleTime, kernelTime, userTime *FileTime) bool { return ret != 0 } -// FILETIME Struct from: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx +// FileTime Struct from: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx type FileTime struct { // DwLowDateTime from Windows API LowDateTime uint32 diff --git a/internal/trigger/http/rest.go b/internal/trigger/http/rest.go index f551c5b31..02d8769ab 100644 --- a/internal/trigger/http/rest.go +++ b/internal/trigger/http/rest.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,70 +24,71 @@ import ( "net/http" "sync" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" ) // Trigger implements Trigger to support Triggers type Trigger struct { - Configuration *common.ConfigurationStruct - Runtime *runtime.GolangRuntime - outputData []byte - Webserver *webserver.WebServer - EdgeXClients common.EdgeXClients + dic *di.Container + Runtime *runtime.GolangRuntime + Webserver *webserver.WebServer + outputData []byte +} + +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime, webserver *webserver.WebServer) *Trigger { + return &Trigger{ + dic: dic, + Runtime: runtime, + Webserver: webserver, + } } // Initialize initializes the Trigger for logging and REST route -func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { - logger := trigger.EdgeXClients.LoggingClient +func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) if background != nil { return nil, errors.New("background publishing not supported for services using HTTP trigger") } - logger.Info("Initializing HTTP Trigger") + lc.Info("Initializing HTTP Trigger") trigger.Webserver.SetupTriggerRoute(internal.ApiTriggerRoute, trigger.requestHandler) - logger.Info("HTTP Trigger Initialized") + lc.Info("HTTP Trigger Initialized") return nil, nil } func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Request) { - defer r.Body.Close() + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) + defer func() { _ = r.Body.Close() }() - logger := trigger.EdgeXClients.LoggingClient contentType := r.Header.Get(clients.ContentType) data, err := ioutil.ReadAll(r.Body) if err != nil { - logger.Error("Error reading HTTP Body", "error", err) + lc.Error("Error reading HTTP Body", "error", err) writer.WriteHeader(http.StatusBadRequest) - writer.Write([]byte(fmt.Sprintf("Error reading HTTP Body: %s", err.Error()))) + _, _ = writer.Write([]byte(fmt.Sprintf("Error reading HTTP Body: %s", err.Error()))) return } - logger.Debug("Request Body read", "byte count", len(data)) + lc.Debug("Request Body read", "byte count", len(data)) correlationID := r.Header.Get(internal.CorrelationHeaderKey) - edgexContext := &appcontext.Context{ - CorrelationID: correlationID, - Configuration: trigger.Configuration, - LoggingClient: trigger.EdgeXClients.LoggingClient, - EventClient: trigger.EdgeXClients.EventClient, - ValueDescriptorClient: trigger.EdgeXClients.ValueDescriptorClient, - CommandClient: trigger.EdgeXClients.CommandClient, - NotificationsClient: trigger.EdgeXClients.NotificationsClient, - } - logger.Trace("Received message from http", clients.CorrelationHeader, correlationID) - logger.Debug("Received message from http", clients.ContentType, contentType) + appContext := appfunction.NewContext(correlationID, trigger.dic, contentType) + + lc.Trace("Received message from http", clients.CorrelationHeader, correlationID) + lc.Debug("Received message from http", clients.ContentType, contentType) envelope := types.MessageEnvelope{ CorrelationID: correlationID, @@ -95,21 +96,26 @@ func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Reque Payload: data, } - messageError := trigger.Runtime.ProcessMessage(edgexContext, envelope) + messageError := trigger.Runtime.ProcessMessage(appContext, envelope) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. writer.WriteHeader(messageError.ErrorCode) - writer.Write([]byte(messageError.Err.Error())) + _, _ = writer.Write([]byte(messageError.Err.Error())) return } - if len(edgexContext.ResponseContentType) > 0 { - writer.Header().Set(clients.ContentType, edgexContext.ResponseContentType) + if len(appContext.ResponseContentType()) > 0 { + writer.Header().Set(clients.ContentType, appContext.ResponseContentType()) + } + + _, err = writer.Write(appContext.ResponseData()) + if err != nil { + lc.Errorf("unable to write ResponseData as HTTP response: %s", err.Error()) + return } - writer.Write(edgexContext.OutputData) - if edgexContext.OutputData != nil { - logger.Trace("Sent http response message", clients.CorrelationHeader, correlationID) + if appContext.ResponseData() != nil { + lc.Trace("Sent http response message", clients.CorrelationHeader, correlationID) } trigger.outputData = nil diff --git a/internal/trigger/http/rest_test.go b/internal/trigger/http/rest_test.go index cddb24366..b7bb3be27 100644 --- a/internal/trigger/http/rest_test.go +++ b/internal/trigger/http/rest_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 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,6 +20,9 @@ package http import ( "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" "github.com/stretchr/testify/assert" @@ -26,7 +30,12 @@ import ( func TestTriggerInitializeWitBackgroundChannel(t *testing.T) { background := make(chan types.MessageEnvelope) - trigger := Trigger{} + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) + trigger := NewTrigger(dic, nil, nil) deferred, err := trigger.Initialize(nil, nil, background) diff --git a/internal/trigger/messagebus/messaging.go b/internal/trigger/messagebus/messaging.go index af43dce03..cf56d3704 100644 --- a/internal/trigger/messagebus/messaging.go +++ b/internal/trigger/messagebus/messaging.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,46 +22,53 @@ import ( "strings" "sync" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/messaging" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" ) // Trigger implements Trigger to support MessageBusData type Trigger struct { - Configuration *common.ConfigurationStruct - Runtime *runtime.GolangRuntime - client messaging.MessageClient - topics []types.TopicChannel - EdgeXClients common.EdgeXClients + dic *di.Container + runtime *runtime.GolangRuntime + topics []types.TopicChannel + client messaging.MessageClient +} + +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime) *Trigger { + return &Trigger{ + dic: dic, + runtime: runtime, + } } // Initialize ... func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { var err error - lc := trigger.EdgeXClients.LoggingClient + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) + config := container.ConfigurationFrom(trigger.dic.Get) - lc.Infof("Initializing Message Bus Trigger for '%s'", trigger.Configuration.Trigger.EdgexMessageBus.Type) + lc.Infof("Initializing Message Bus Trigger for '%s'", config.Trigger.EdgexMessageBus.Type) - trigger.client, err = messaging.NewMessageClient(trigger.Configuration.Trigger.EdgexMessageBus) + trigger.client, err = messaging.NewMessageClient(config.Trigger.EdgexMessageBus) if err != nil { return nil, err } - if len(strings.TrimSpace(trigger.Configuration.Trigger.SubscribeTopics)) == 0 { + if len(strings.TrimSpace(config.Trigger.SubscribeTopics)) == 0 { // Still allows subscribing to blank topic to receive all messages - trigger.topics = append(trigger.topics, types.TopicChannel{Topic: trigger.Configuration.Trigger.SubscribeTopics, Messages: make(chan types.MessageEnvelope)}) + trigger.topics = append(trigger.topics, types.TopicChannel{Topic: config.Trigger.SubscribeTopics, Messages: make(chan types.MessageEnvelope)}) } else { - topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(trigger.Configuration.Trigger.SubscribeTopics, util.SplitComma)) + topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(config.Trigger.SubscribeTopics, util.SplitComma)) for _, topic := range topics { trigger.topics = append(trigger.topics, types.TopicChannel{Topic: topic, Messages: make(chan types.MessageEnvelope)}) } @@ -75,17 +82,17 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context } lc.Infof("Subscribing to topic(s): '%s' @ %s://%s:%d", - trigger.Configuration.Trigger.SubscribeTopics, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Protocol, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Host, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Port) + config.Trigger.SubscribeTopics, + config.Trigger.EdgexMessageBus.SubscribeHost.Protocol, + config.Trigger.EdgexMessageBus.SubscribeHost.Host, + config.Trigger.EdgexMessageBus.SubscribeHost.Port) - if len(trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Host) > 0 { + if len(config.Trigger.EdgexMessageBus.PublishHost.Host) > 0 { lc.Infof("Publishing to topic: '%s' @ %s://%s:%d", - trigger.Configuration.Trigger.PublishTopic, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Protocol, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Host, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Port) + config.Trigger.PublishTopic, + config.Trigger.EdgexMessageBus.PublishHost.Protocol, + config.Trigger.EdgexMessageBus.PublishHost.Host, + config.Trigger.EdgexMessageBus.PublishHost.Port) } // Need to have a go func for each subscription so we know with topic the data was received for. @@ -122,13 +129,13 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context case bg := <-background: go func() { - err := trigger.client.Publish(bg, trigger.Configuration.Trigger.PublishTopic) + err := trigger.client.Publish(bg, config.Trigger.PublishTopic) if err != nil { lc.Errorf("Failed to publish background Message to bus, %v", err) return } - lc.Debugf("Published background message to bus on %s topic", trigger.Configuration.Trigger.PublishTopic) + lc.Debugf("Published background message to bus on %s topic", config.Trigger.PublishTopic) lc.Tracef("%s=%s", clients.CorrelationHeader, bg.CorrelationID) }() } @@ -136,7 +143,7 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context }() if err := trigger.client.Subscribe(trigger.topics, messageErrors); err != nil { - return nil, fmt.Errorf("failed to subscribe to topic(s) '%s': %s", trigger.Configuration.Trigger.SubscribeTopics, err.Error()) + return nil, fmt.Errorf("failed to subscribe to topic(s) '%s': %s", config.Trigger.SubscribeTopics, err.Error()) } deferred := func() { @@ -153,46 +160,41 @@ func (trigger *Trigger) processMessage(logger logger.LoggingClient, triggerTopic logger.Debugf("Received message from MessageBus on topic '%s'. Content-Type=%s", triggerTopic.Topic, message.ContentType) logger.Tracef("%s=%s", clients.CorrelationHeader, message.CorrelationID) - edgexContext := &appcontext.Context{ - CorrelationID: message.CorrelationID, - Configuration: trigger.Configuration, - LoggingClient: trigger.EdgeXClients.LoggingClient, - EventClient: trigger.EdgeXClients.EventClient, - ValueDescriptorClient: trigger.EdgeXClients.ValueDescriptorClient, - CommandClient: trigger.EdgeXClients.CommandClient, - NotificationsClient: trigger.EdgeXClients.NotificationsClient, - } + appContext := appfunction.NewContext(message.CorrelationID, trigger.dic, message.ContentType) - messageError := trigger.Runtime.ProcessMessage(edgexContext, message) + messageError := trigger.runtime.ProcessMessage(appContext, message) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. return } - if edgexContext.OutputData != nil { + if appContext.ResponseData() != nil { var contentType string - if edgexContext.ResponseContentType != "" { - contentType = edgexContext.ResponseContentType + if appContext.ResponseContentType() != "" { + contentType = appContext.ResponseContentType() } else { contentType = clients.ContentTypeJSON - if edgexContext.OutputData[0] != byte('{') { + if appContext.ResponseData()[0] != byte('{') { // If not JSON then assume it is CBOR contentType = clients.ContentTypeCBOR } } outputEnvelope := types.MessageEnvelope{ - CorrelationID: edgexContext.CorrelationID, - Payload: edgexContext.OutputData, + CorrelationID: appContext.CorrelationID(), + Payload: appContext.ResponseData(), ContentType: contentType, } - err := trigger.client.Publish(outputEnvelope, trigger.Configuration.Trigger.PublishTopic) + + config := container.ConfigurationFrom(trigger.dic.Get) + + err := trigger.client.Publish(outputEnvelope, config.Trigger.PublishTopic) if err != nil { logger.Errorf("Failed to publish Message to bus, %v", err) return } - logger.Debugf("Published message to bus on '%s' topic", trigger.Configuration.Trigger.PublishTopic) + logger.Debugf("Published message to bus on '%s' topic", config.Trigger.PublishTopic) logger.Tracef("%s=%s", clients.CorrelationHeader, message.CorrelationID) } } diff --git a/internal/trigger/messagebus/messaging_test.go b/internal/trigger/messagebus/messaging_test.go index a99472c43..2128d2fe8 100644 --- a/internal/trigger/messagebus/messaging_test.go +++ b/internal/trigger/messagebus/messaging_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,11 +23,14 @@ import ( "testing" "time" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" @@ -44,8 +47,6 @@ import ( // Note the constant TriggerTypeMessageBus can not be used due to cyclic imports const TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" -var lc logger.LoggingClient - var addEventRequest = createTestEventRequest() var expectedEvent = addEventRequest.Event @@ -56,8 +57,14 @@ func createTestEventRequest() requests.AddEventRequest { return request } +var dic *di.Container + func TestMain(m *testing.M) { - lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) m.Run() } @@ -85,10 +92,18 @@ func TestInitialize(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + goRuntime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + trigger := NewTrigger(dic, goRuntime) + + _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) assert.NotNil(t, trigger.client, "Expected client to be set") assert.Equal(t, 1, len(trigger.topics)) assert.Equal(t, "events", trigger.topics[0].Topic) @@ -119,9 +134,15 @@ func TestInitializeBadConfiguration(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + goRuntime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + trigger := NewTrigger(dic, goRuntime) _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) assert.Error(t, err) } @@ -149,21 +170,28 @@ func TestInitializeAndProcessEventWithNoOutput(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) + _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) payload, err := json.Marshal(addEventRequest) require.NoError(t, err) @@ -185,13 +213,16 @@ func TestInitializeAndProcessEventWithNoOutput(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) require.NoError(t, err, "Unable to create to publisher") - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "") //transform1 should be called after this executes require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - assert.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } } func TestInitializeAndProcessEventWithOutput(t *testing.T) { @@ -217,25 +248,31 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + responseContentType := uuid.New().String() expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.ResponseContentType = responseContentType - edgexcontext.Complete([]byte("Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseContentType(responseContentType) + appContext.SetResponseData([]byte("Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -253,7 +290,7 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -270,13 +307,15 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") - + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true for receiveMessage { @@ -315,22 +354,28 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.Complete([]byte("{;)Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseData([]byte("{;)Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -348,7 +393,7 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -365,12 +410,15 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true @@ -410,22 +458,27 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.Complete([]byte("Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseData([]byte("Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ Host: "localhost", @@ -442,12 +495,13 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus require.NoError(t, err) - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + _, err = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) payload, _ := json.Marshal(addEventRequest) @@ -457,12 +511,15 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true @@ -502,13 +559,19 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" expectedPayload := []byte(`{"id":"5888dea1bd36573f4681d6f9","created":1485364897029,"modified":1485364897029,"origin":1471806386919,"pushed":0,"device":"livingroomthermostat","readings":[{"id":"5888dea0bd36573f4681d6f8","created":1485364896983,"modified":1485364896983,"origin":1471806386919,"pushed":0,"name":"temperature","value":"38","device":"livingroomthermostat"}]}`) goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -526,7 +589,7 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -534,7 +597,8 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { background := make(chan types.MessageEnvelope) - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), background) + _, err = trigger.Initialize(&sync.WaitGroup{}, context.Background(), background) + require.NoError(t, err) message := types.MessageEnvelope{ CorrelationID: expectedCorrelationID, @@ -580,19 +644,25 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - done := make(chan bool) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - require.Equal(t, expectedEvent, params[0]) - done <- true + transformWasCalled := make(chan bool, 1) + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + require.Equal(t, expectedEvent, data) + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) require.NoError(t, err) @@ -620,7 +690,7 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { require.NoError(t, err, "Failed to publish message") select { - case <-done: + case <-transformWasCalled: // do nothing, just need to fall out. case <-time.After(3 * time.Second): require.Fail(t, "Transform never called for t1") @@ -630,7 +700,7 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { require.NoError(t, err, "Failed to publish message") select { - case <-done: + case <-transformWasCalled: // do nothing, just need to fall out. case <-time.After(3 * time.Second): require.Fail(t, "Transform never called t2") diff --git a/internal/trigger/mqtt/mqtt.go b/internal/trigger/mqtt/mqtt.go index 167aec735..f89394729 100644 --- a/internal/trigger/mqtt/mqtt.go +++ b/internal/trigger/mqtt/mqtt.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,50 +25,48 @@ import ( "sync" "time" - pahoMqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/google/uuid" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + pahoMqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/google/uuid" ) // Trigger implements Trigger to support Triggers type Trigger struct { - configuration *common.ConfigurationStruct - mqttClient pahoMqtt.Client - runtime *runtime.GolangRuntime - edgeXClients common.EdgeXClients - secretProvider interfaces.SecretProvider + dic *di.Container + lc logger.LoggingClient + mqttClient pahoMqtt.Client + runtime *runtime.GolangRuntime } -func NewTrigger( - configuration *common.ConfigurationStruct, - runtime *runtime.GolangRuntime, - clients common.EdgeXClients, - secretProvider interfaces.SecretProvider) *Trigger { +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime) *Trigger { return &Trigger{ - configuration: configuration, - runtime: runtime, - edgeXClients: clients, - secretProvider: secretProvider, + dic: dic, + runtime: runtime, + lc: bootstrapContainer.LoggingClientFrom(dic.Get), } } // Initialize initializes the Trigger for an external MQTT broker func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - brokerConfig := trigger.configuration.Trigger.ExternalMqtt - topics := trigger.configuration.Trigger.SubscribeTopics + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + brokerConfig := config.Trigger.ExternalMqtt + topics := config.Trigger.SubscribeTopics - logger.Info("Initializing MQTT Trigger") + lc.Info("Initializing MQTT Trigger") if background != nil { return nil, errors.New("background publishing not supported for services using MQTT trigger") @@ -80,7 +78,7 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro brokerUrl, err := url.Parse(brokerConfig.Url) if err != nil { - return nil, fmt.Errorf("invalid MQTT Broker Url '%s': %s", trigger.configuration.Trigger.ExternalMqtt.Url, err.Error()) + return nil, fmt.Errorf("invalid MQTT Broker Url '%s': %s", config.Trigger.ExternalMqtt.Url, err.Error()) } opts := pahoMqtt.NewClientOptions() @@ -97,9 +95,10 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro opts.KeepAlive = brokerConfig.KeepAlive opts.Servers = []*url.URL{brokerUrl} + // Since this factory is shared between the MQTT pipeline function and this trigger we must provide + // a dummy AppFunctionContext which will provide access to GetSecret mqttFactory := secure.NewMqttFactory( - logger, - trigger.secretProvider, + appfunction.NewContext("", trigger.dic, ""), brokerConfig.AuthMode, brokerConfig.SecretPath, brokerConfig.SkipCertVerify, @@ -110,16 +109,16 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro return nil, fmt.Errorf("unable to create secure MQTT Client: %s", err.Error()) } - logger.Info(fmt.Sprintf("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl)) + lc.Info(fmt.Sprintf("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl)) if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { return nil, fmt.Errorf("could not connect to broker for MQTT trigger: %s", token.Error().Error()) } - logger.Info("Connected to mqtt server for MQTT trigger") + lc.Info("Connected to mqtt server for MQTT trigger") deferred := func() { - logger.Info("Disconnecting from broker for MQTT trigger") + lc.Info("Disconnecting from broker for MQTT trigger") trigger.mqttClient.Disconnect(0) } @@ -130,27 +129,29 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro func (trigger *Trigger) onConnectHandler(mqttClient pahoMqtt.Client) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(trigger.configuration.Trigger.SubscribeTopics, util.SplitComma)) - qos := trigger.configuration.Trigger.ExternalMqtt.QoS + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(config.Trigger.SubscribeTopics, util.SplitComma)) + qos := config.Trigger.ExternalMqtt.QoS for _, topic := range topics { if token := mqttClient.Subscribe(topic, qos, trigger.messageHandler); token.Wait() && token.Error() != nil { mqttClient.Disconnect(0) - logger.Error(fmt.Sprintf("could not subscribe to topic '%s' for MQTT trigger: %s", + lc.Error(fmt.Sprintf("could not subscribe to topic '%s' for MQTT trigger: %s", topic, token.Error().Error())) return } } - logger.Infof("Subscribed to topic(s) '%s' for MQTT trigger", trigger.configuration.Trigger.SubscribeTopics) + lc.Infof("Subscribed to topic(s) '%s' for MQTT trigger", config.Trigger.SubscribeTopics) } func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt.Message) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - brokerConfig := trigger.configuration.Trigger.ExternalMqtt - topic := trigger.configuration.Trigger.PublishTopic + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + brokerConfig := config.Trigger.ExternalMqtt + topic := config.Trigger.PublishTopic data := message.Payload() contentType := clients.ContentTypeJSON @@ -161,18 +162,10 @@ func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt. correlationID := uuid.New().String() - edgexContext := &appcontext.Context{ - CorrelationID: correlationID, - Configuration: trigger.configuration, - LoggingClient: trigger.edgeXClients.LoggingClient, - EventClient: trigger.edgeXClients.EventClient, - ValueDescriptorClient: trigger.edgeXClients.ValueDescriptorClient, - CommandClient: trigger.edgeXClients.CommandClient, - NotificationsClient: trigger.edgeXClients.NotificationsClient, - } + appContext := appfunction.NewContext(correlationID, trigger.dic, contentType) - logger.Debugf("Received message from MQTT Trigger with %d bytes from topic '%s'. Content-Type=%s", len(data), message.Topic(), contentType) - logger.Tracef("%s=%s", clients.CorrelationHeader, correlationID) + lc.Debugf("Received message from MQTT Trigger with %d bytes from topic '%s'. Content-Type=%s", len(data), message.Topic(), contentType) + lc.Tracef("%s=%s", clients.CorrelationHeader, correlationID) envelope := types.MessageEnvelope{ CorrelationID: correlationID, @@ -180,19 +173,19 @@ func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt. Payload: data, } - messageError := trigger.runtime.ProcessMessage(edgexContext, envelope) + messageError := trigger.runtime.ProcessMessage(appContext, envelope) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. // ToDo: Do we want to publish the error back to the Broker? return } - if len(edgexContext.OutputData) > 0 && len(topic) > 0 { - if token := client.Publish(topic, brokerConfig.QoS, brokerConfig.Retain, edgexContext.OutputData); token.Wait() && token.Error() != nil { - logger.Error("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) + if len(appContext.ResponseData()) > 0 && len(topic) > 0 { + if token := client.Publish(topic, brokerConfig.QoS, brokerConfig.Retain, appContext.ResponseData); token.Wait() && token.Error() != nil { + lc.Error("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) } else { - logger.Trace("Sent MQTT Trigger response message", clients.CorrelationHeader, correlationID) - logger.Debug(fmt.Sprintf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(edgexContext.OutputData))) + lc.Trace("Sent MQTT Trigger response message", clients.CorrelationHeader, correlationID) + lc.Debug(fmt.Sprintf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(appContext.ResponseData()))) } } } diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 77974fa4b..ae82f1de3 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,10 +21,12 @@ import ( "net/http" "time" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" contracts "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/controller/rest" @@ -35,11 +37,10 @@ import ( // WebServer handles the webserver configuration type WebServer struct { - Config *common.ConfigurationStruct - lc logger.LoggingClient - router *mux.Router - secretProvider interfaces.SecretProvider - controller *rest.Controller + config *common.ConfigurationStruct + lc logger.LoggingClient + router *mux.Router + controller *rest.Controller } // swagger:model @@ -49,13 +50,12 @@ type Version struct { } // NewWebserver returns a new instance of *WebServer -func NewWebServer(config *common.ConfigurationStruct, secretProvider interfaces.SecretProvider, lc logger.LoggingClient, router *mux.Router) *WebServer { +func NewWebServer(dic *di.Container, router *mux.Router) *WebServer { ws := &WebServer{ - Config: config, - lc: lc, - router: router, - secretProvider: secretProvider, - controller: rest.NewController(router, lc, config, secretProvider), + lc: bootstrapContainer.LoggingClientFrom(dic.Get), + config: container.ConfigurationFrom(dic.Get), + router: router, + controller: rest.NewController(router, dic), } return ws @@ -95,7 +95,7 @@ func (webserver *WebServer) SetupTriggerRoute(path string, handlerForTrigger fun // StartWebServer starts the web server func (webserver *WebServer) StartWebServer(errChannel chan error) { go func() { - if serviceTimeout, err := time.ParseDuration(webserver.Config.Service.Timeout); err != nil { + if serviceTimeout, err := time.ParseDuration(webserver.config.Service.Timeout); err != nil { errChannel <- fmt.Errorf("failed to parse Service.Timeout: %v", err) } else { listenAndServe(webserver, serviceTimeout, errChannel) @@ -108,11 +108,11 @@ func listenAndServe(webserver *WebServer, serviceTimeout time.Duration, errChann // this allows env overrides to explicitly set the value used // for ListenAndServe, as needed for different deployments - addr := fmt.Sprintf("%v:%d", webserver.Config.Service.ServerBindAddr, webserver.Config.Service.Port) + addr := fmt.Sprintf("%v:%d", webserver.config.Service.ServerBindAddr, webserver.config.Service.Port) - if webserver.Config.Service.Protocol == "https" { + if webserver.config.Service.Protocol == "https" { webserver.lc.Infof("Starting HTTPS Web Server on address %v", addr) - errChannel <- http.ListenAndServeTLS(addr, webserver.Config.Service.HTTPSCert, webserver.Config.Service.HTTPSKey, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) + errChannel <- http.ListenAndServeTLS(addr, webserver.config.Service.HTTPSCert, webserver.config.Service.HTTPSKey, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) } else { webserver.lc.Infof("Starting HTTP Web Server on address %v", addr) errChannel <- http.ListenAndServe(addr, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index 3f945a00a..5a9e64d19 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,32 +19,45 @@ package webserver import ( "net/http" "net/http/httptest" + "os" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) -var logClient logger.LoggingClient -var config *common.ConfigurationStruct +var dic *di.Container func TestMain(m *testing.M) { - logClient = logger.NewMockClient() - config = &common.ConfigurationStruct{} - m.Run() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return &mocks.SecretProvider{} + }, + }) + + os.Exit(m.Run()) } func TestAddRoute(t *testing.T) { routePath := "/testRoute" testHandler := func(_ http.ResponseWriter, _ *http.Request) {} - sp := &mocks.SecretProvider{} - webserver := NewWebServer(config, sp, logClient, mux.NewRouter()) + webserver := NewWebServer(dic, mux.NewRouter()) err := webserver.AddRoute(routePath, testHandler) assert.NoError(t, err, "Not expecting an error") @@ -55,13 +68,13 @@ func TestAddRoute(t *testing.T) { } func TestSetupTriggerRoute(t *testing.T) { - sp := &mocks.SecretProvider{} - webserver := NewWebServer(config, sp, logClient, mux.NewRouter()) + webserver := NewWebServer(dic, mux.NewRouter()) handlerFunctionNotCalled := true handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("test")) + _, err := w.Write([]byte("test")) + require.NoError(t, err) handlerFunctionNotCalled = false } diff --git a/pkg/factory.go b/pkg/factory.go new file mode 100644 index 000000000..1fc7030be --- /dev/null +++ b/pkg/factory.go @@ -0,0 +1,61 @@ +// +// Copyright (c) 2021 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 pkg + +import ( + "fmt" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/app" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) + +// NewAppService creates and returns a new ApplicationService with the default TargetType +func NewAppService(serviceKey string) (interfaces.ApplicationService, bool) { + return NewAppServiceWithTargetType(serviceKey, nil) +} + +// NewAppService creates and returns a new ApplicationService with the specified TargetType +func NewAppServiceWithTargetType(serviceKey string, targetType interface{}) (interfaces.ApplicationService, bool) { + service := app.NewService(serviceKey, targetType, interfaces.ProfileSuffixPlaceholder) + if err := service.Initialize(); err != nil { + err = fmt.Errorf("initialization failed: %s", err.Error()) + service.LoggingClient().Errorf("App Service %s", err.Error()) + return nil, false + } + + return service, true +} + +// NewAppFuncContextForTest creates and returns a new AppFunctionContext to be used in unit tests for custom pipeline functions +func NewAppFuncContextForTest(correlationID string, lc logger.LoggingClient) interfaces.AppFunctionContext { + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + }) + return appfunction.NewContext(correlationID, dic, "") +} diff --git a/pkg/factory_test.go b/pkg/factory_test.go new file mode 100644 index 000000000..2e45b3d01 --- /dev/null +++ b/pkg/factory_test.go @@ -0,0 +1,56 @@ +// +// Copyright (c) 2021 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 pkg + +import ( + "os" + "testing" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +var expectedLogger logger.LoggingClient +var expectedCorrelationId string + +var dic *di.Container + +func TestMain(m *testing.M) { + expectedCorrelationId = uuid.NewString() + expectedLogger = logger.NewMockClient() + + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return expectedLogger + }, + }) + + os.Exit(m.Run()) +} + +func TestNewAppFuncContextForTest(t *testing.T) { + expectedContentType := "" + + target := NewAppFuncContextForTest(expectedCorrelationId, expectedLogger) + + assert.Equal(t, expectedLogger, target.LoggingClient()) + assert.Equal(t, expectedCorrelationId, target.CorrelationID()) + assert.Equal(t, expectedContentType, target.InputContentType()) +} diff --git a/pkg/interfaces/backgroundpublisher.go b/pkg/interfaces/backgroundpublisher.go new file mode 100644 index 000000000..a07fb820b --- /dev/null +++ b/pkg/interfaces/backgroundpublisher.go @@ -0,0 +1,25 @@ +// +// Copyright (c) 2020 Technotects +// Copyright (c) 2021 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 interfaces + +// BackgroundPublisher provides an interface to send messages from background processes +// through the service's configured MessageBus output +type BackgroundPublisher interface { + // Publish provided message through the configured MessageBus output + Publish(payload []byte, correlationID string, contentType string) +} diff --git a/pkg/interfaces/context.go b/pkg/interfaces/context.go new file mode 100644 index 000000000..376b336d1 --- /dev/null +++ b/pkg/interfaces/context.go @@ -0,0 +1,77 @@ +// +// Copyright (c) 2021 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 interfaces + +import ( + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" +) + +// AppFunction is a type alias for a application pipeline function. +// appCtx is a reference to the AppFunctionContext below. +// data is the data to be operated on by the function. +// bool return value indicates if the pipeline should continue executing (true) or not (false) +// interface{} is either the data to pass to the next function (continue executing) or +// an error (stop executing due to error) or nil (done executing) +type AppFunction = func(appCxt AppFunctionContext, data interface{}) (bool, interface{}) + +// AppFunctionContext defines the interface for an Edgex Application Service Context provided to +// App Functions when executing in the Functions Pipeline. +type AppFunctionContext interface { + // CorrelationID returns the correlation ID associated with the context. + CorrelationID() string + // InputContentType returns the content type of the data that initiated the pipeline execution. Only useful when + // the TargetType for the pipeline is []byte, otherwise the data with be the type specified by TargetType. + InputContentType() string + // SetResponseData sets the response data that will be returned to the trigger when pipeline execution is complete. + SetResponseData(data []byte) + // SetResponseContentType sets the content type that will be returned to the trigger when pipeline + // execution is complete. + SetResponseContentType(string) + // ResponseContentType returns the content type that will be returned to the trigger when pipeline + // execution is complete. + ResponseContentType() string + // SetRetryData set the data that is to be retried later as part of the Store and Forward capability. + // Used when there was failure sending the data to an external source. + SetRetryData(data []byte) + // GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. + // An error is returned if the path is not found or any of the keys (if specified) are not found. + // Omit keys if all secret data for the specified path is required. + GetSecret(path string, keys ...string) (map[string]string, error) + // SecretsLastUpdated returns that timestamp for when the secrets in the SecretStore where last updated. + // Useful when a connection to external source needs to be redone when the credentials have been updated. + SecretsLastUpdated() time.Time + // LoggingClient returns the Logger client + LoggingClient() logger.LoggingClient + // EventClient returns the Event client. Note if Core Data is not specified in the Clients configuration, + // this will return nil. + EventClient() coredata.EventClient + // CommandClient returns the Command client. Note if Support Command is not specified in the Clients configuration, + // this will return nil. + CommandClient() command.CommandClient + // NotificationsClient returns the Notifications client. Note if Support Notifications is not specified in the + // Clients configuration, this will return nil. + NotificationsClient() notifications.NotificationsClient + // PushToCoreData is a convenience function for adding new Event/Reading(s) to core data and + // back onto the EdgeX MessageBus. This function uses the Event client and will result in an error if + // Core Data is not specified in the Clients configuration + PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) +} diff --git a/pkg/interfaces/mocks/AppFunctionContext.go b/pkg/interfaces/mocks/AppFunctionContext.go new file mode 100644 index 000000000..646c33e70 --- /dev/null +++ b/pkg/interfaces/mocks/AppFunctionContext.go @@ -0,0 +1,211 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + command "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + coredata "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + + dtos "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + logger "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + mock "github.com/stretchr/testify/mock" + + notifications "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + time "time" +) + +// AppFunctionContext is an autogenerated mock type for the AppFunctionContext type +type AppFunctionContext struct { + mock.Mock +} + +// CommandClient provides a mock function with given fields: +func (_m *AppFunctionContext) CommandClient() command.CommandClient { + ret := _m.Called() + + var r0 command.CommandClient + if rf, ok := ret.Get(0).(func() command.CommandClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(command.CommandClient) + } + } + + return r0 +} + +// CorrelationID provides a mock function with given fields: +func (_m *AppFunctionContext) CorrelationID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// EventClient provides a mock function with given fields: +func (_m *AppFunctionContext) EventClient() coredata.EventClient { + ret := _m.Called() + + var r0 coredata.EventClient + if rf, ok := ret.Get(0).(func() coredata.EventClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(coredata.EventClient) + } + } + + return r0 +} + +// GetSecret provides a mock function with given fields: path, keys +func (_m *AppFunctionContext) GetSecret(path string, keys ...string) (map[string]string, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, path) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(string, ...string) map[string]string); ok { + r0 = rf(path, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(path, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InputContentType provides a mock function with given fields: +func (_m *AppFunctionContext) InputContentType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// LoggingClient provides a mock function with given fields: +func (_m *AppFunctionContext) LoggingClient() logger.LoggingClient { + ret := _m.Called() + + var r0 logger.LoggingClient + if rf, ok := ret.Get(0).(func() logger.LoggingClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.LoggingClient) + } + } + + return r0 +} + +// NotificationsClient provides a mock function with given fields: +func (_m *AppFunctionContext) NotificationsClient() notifications.NotificationsClient { + ret := _m.Called() + + var r0 notifications.NotificationsClient + if rf, ok := ret.Get(0).(func() notifications.NotificationsClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(notifications.NotificationsClient) + } + } + + return r0 +} + +// PushToCoreData provides a mock function with given fields: deviceName, readingName, value +func (_m *AppFunctionContext) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { + ret := _m.Called(deviceName, readingName, value) + + var r0 *dtos.Event + if rf, ok := ret.Get(0).(func(string, string, interface{}) *dtos.Event); ok { + r0 = rf(deviceName, readingName, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dtos.Event) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, interface{}) error); ok { + r1 = rf(deviceName, readingName, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ResponseContentType provides a mock function with given fields: +func (_m *AppFunctionContext) ResponseContentType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// SecretsLastUpdated provides a mock function with given fields: +func (_m *AppFunctionContext) SecretsLastUpdated() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// SetResponseContentType provides a mock function with given fields: _a0 +func (_m *AppFunctionContext) SetResponseContentType(_a0 string) { + _m.Called(_a0) +} + +// SetResponseData provides a mock function with given fields: data +func (_m *AppFunctionContext) SetResponseData(data []byte) { + _m.Called(data) +} + +// SetRetryData provides a mock function with given fields: data +func (_m *AppFunctionContext) SetRetryData(data []byte) { + _m.Called(data) +} diff --git a/pkg/interfaces/mocks/ApplicationService.go b/pkg/interfaces/mocks/ApplicationService.go new file mode 100644 index 000000000..7f9f8a6f0 --- /dev/null +++ b/pkg/interfaces/mocks/ApplicationService.go @@ -0,0 +1,301 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + command "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + coredata "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + + http "net/http" + + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + + logger "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + mock "github.com/stretchr/testify/mock" + + notifications "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + registry "github.com/edgexfoundry/go-mod-registry/v2/registry" +) + +// ApplicationService is an autogenerated mock type for the ApplicationService type +type ApplicationService struct { + mock.Mock +} + +// AddBackgroundPublisher provides a mock function with given fields: capacity +func (_m *ApplicationService) AddBackgroundPublisher(capacity int) interfaces.BackgroundPublisher { + ret := _m.Called(capacity) + + var r0 interfaces.BackgroundPublisher + if rf, ok := ret.Get(0).(func(int) interfaces.BackgroundPublisher); ok { + r0 = rf(capacity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interfaces.BackgroundPublisher) + } + } + + return r0 +} + +// AddRoute provides a mock function with given fields: route, handler, methods +func (_m *ApplicationService) AddRoute(route string, handler func(http.ResponseWriter, *http.Request), methods ...string) error { + _va := make([]interface{}, len(methods)) + for _i := range methods { + _va[_i] = methods[_i] + } + var _ca []interface{} + _ca = append(_ca, route, handler) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func(http.ResponseWriter, *http.Request), ...string) error); ok { + r0 = rf(route, handler, methods...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ApplicationSettings provides a mock function with given fields: +func (_m *ApplicationService) ApplicationSettings() map[string]string { + ret := _m.Called() + + var r0 map[string]string + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + return r0 +} + +// CommandClient provides a mock function with given fields: +func (_m *ApplicationService) CommandClient() command.CommandClient { + ret := _m.Called() + + var r0 command.CommandClient + if rf, ok := ret.Get(0).(func() command.CommandClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(command.CommandClient) + } + } + + return r0 +} + +// EventClient provides a mock function with given fields: +func (_m *ApplicationService) EventClient() coredata.EventClient { + ret := _m.Called() + + var r0 coredata.EventClient + if rf, ok := ret.Get(0).(func() coredata.EventClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(coredata.EventClient) + } + } + + return r0 +} + +// GetAppSettingStrings provides a mock function with given fields: setting +func (_m *ApplicationService) GetAppSettingStrings(setting string) ([]string, error) { + ret := _m.Called(setting) + + var r0 []string + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(setting) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(setting) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSecret provides a mock function with given fields: path, keys +func (_m *ApplicationService) GetSecret(path string, keys ...string) (map[string]string, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, path) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(string, ...string) map[string]string); ok { + r0 = rf(path, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(path, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoadConfigurablePipeline provides a mock function with given fields: +func (_m *ApplicationService) LoadConfigurablePipeline() ([]func(interfaces.AppFunctionContext, interface{}) (bool, interface{}), error) { + ret := _m.Called() + + var r0 []func(interfaces.AppFunctionContext, interface{}) (bool, interface{}) + if rf, ok := ret.Get(0).(func() []func(interfaces.AppFunctionContext, interface{}) (bool, interface{})); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoggingClient provides a mock function with given fields: +func (_m *ApplicationService) LoggingClient() logger.LoggingClient { + ret := _m.Called() + + var r0 logger.LoggingClient + if rf, ok := ret.Get(0).(func() logger.LoggingClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.LoggingClient) + } + } + + return r0 +} + +// MakeItRun provides a mock function with given fields: +func (_m *ApplicationService) MakeItRun() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MakeItStop provides a mock function with given fields: +func (_m *ApplicationService) MakeItStop() { + _m.Called() +} + +// NotificationsClient provides a mock function with given fields: +func (_m *ApplicationService) NotificationsClient() notifications.NotificationsClient { + ret := _m.Called() + + var r0 notifications.NotificationsClient + if rf, ok := ret.Get(0).(func() notifications.NotificationsClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(notifications.NotificationsClient) + } + } + + return r0 +} + +// RegisterCustomTriggerFactory provides a mock function with given fields: name, factory +func (_m *ApplicationService) RegisterCustomTriggerFactory(name string, factory func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error { + ret := _m.Called(name, factory) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error); ok { + r0 = rf(name, factory) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RegistryClient provides a mock function with given fields: +func (_m *ApplicationService) RegistryClient() registry.Client { + ret := _m.Called() + + var r0 registry.Client + if rf, ok := ret.Get(0).(func() registry.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Client) + } + } + + return r0 +} + +// SetFunctionsPipeline provides a mock function with given fields: transforms +func (_m *ApplicationService) SetFunctionsPipeline(transforms ...func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) error { + _va := make([]interface{}, len(transforms)) + for _i := range transforms { + _va[_i] = transforms[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) error); ok { + r0 = rf(transforms...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StoreSecret provides a mock function with given fields: path, secretData +func (_m *ApplicationService) StoreSecret(path string, secretData map[string]string) error { + ret := _m.Called(path, secretData) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = rf(path, secretData) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/interfaces/mocks/BackgroundPublisher.go b/pkg/interfaces/mocks/BackgroundPublisher.go new file mode 100644 index 000000000..f99bb27e9 --- /dev/null +++ b/pkg/interfaces/mocks/BackgroundPublisher.go @@ -0,0 +1,15 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// BackgroundPublisher is an autogenerated mock type for the BackgroundPublisher type +type BackgroundPublisher struct { + mock.Mock +} + +// Publish provides a mock function with given fields: payload, correlationID, contentType +func (_m *BackgroundPublisher) Publish(payload []byte, correlationID string, contentType string) { + _m.Called(payload, correlationID, contentType) +} diff --git a/pkg/interfaces/mocks/Trigger.go b/pkg/interfaces/mocks/Trigger.go new file mode 100644 index 000000000..362b82569 --- /dev/null +++ b/pkg/interfaces/mocks/Trigger.go @@ -0,0 +1,43 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + bootstrap "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + + mock "github.com/stretchr/testify/mock" + + sync "sync" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// Trigger is an autogenerated mock type for the Trigger type +type Trigger struct { + mock.Mock +} + +// Initialize provides a mock function with given fields: wg, ctx, background +func (_m *Trigger) Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { + ret := _m.Called(wg, ctx, background) + + var r0 bootstrap.Deferred + if rf, ok := ret.Get(0).(func(*sync.WaitGroup, context.Context, <-chan types.MessageEnvelope) bootstrap.Deferred); ok { + r0 = rf(wg, ctx, background) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(bootstrap.Deferred) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*sync.WaitGroup, context.Context, <-chan types.MessageEnvelope) error); ok { + r1 = rf(wg, ctx, background) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/interfaces/mocks/TriggerContextBuilder.go b/pkg/interfaces/mocks/TriggerContextBuilder.go new file mode 100644 index 000000000..b136b826d --- /dev/null +++ b/pkg/interfaces/mocks/TriggerContextBuilder.go @@ -0,0 +1,31 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + mock "github.com/stretchr/testify/mock" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// TriggerContextBuilder is an autogenerated mock type for the TriggerContextBuilder type +type TriggerContextBuilder struct { + mock.Mock +} + +// Execute provides a mock function with given fields: env +func (_m *TriggerContextBuilder) Execute(env types.MessageEnvelope) interfaces.AppFunctionContext { + ret := _m.Called(env) + + var r0 interfaces.AppFunctionContext + if rf, ok := ret.Get(0).(func(types.MessageEnvelope) interfaces.AppFunctionContext); ok { + r0 = rf(env) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interfaces.AppFunctionContext) + } + } + + return r0 +} diff --git a/pkg/interfaces/mocks/TriggerMessageProcessor.go b/pkg/interfaces/mocks/TriggerMessageProcessor.go new file mode 100644 index 000000000..d758ee2e1 --- /dev/null +++ b/pkg/interfaces/mocks/TriggerMessageProcessor.go @@ -0,0 +1,29 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + mock "github.com/stretchr/testify/mock" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// TriggerMessageProcessor is an autogenerated mock type for the TriggerMessageProcessor type +type TriggerMessageProcessor struct { + mock.Mock +} + +// Execute provides a mock function with given fields: ctx, envelope +func (_m *TriggerMessageProcessor) Execute(ctx interfaces.AppFunctionContext, envelope types.MessageEnvelope) error { + ret := _m.Called(ctx, envelope) + + var r0 error + if rf, ok := ret.Get(0).(func(interfaces.AppFunctionContext, types.MessageEnvelope) error); ok { + r0 = rf(ctx, envelope) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/interfaces/service.go b/pkg/interfaces/service.go new file mode 100644 index 000000000..475533111 --- /dev/null +++ b/pkg/interfaces/service.go @@ -0,0 +1,102 @@ +// +// Copyright (c) 2021 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 interfaces + +import ( + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-registry/v2/registry" +) + +const ( + // AppServiceContextKey is the context key for getting the reference to the ApplicationService from the context passed to + // a custom REST Handler + AppServiceContextKey = "AppService" + + // ProfileSuffixPlaceholder is the placeholder text to use in an application service's service key if the + // the name of the configuration profile used is to be used in the service's service key. + // Only useful if the service has multiple configuration profiles to choose from at runtime. + // Example: + // const ( + // serviceKey = "MyServiceName-" + interfaces.ProfileSuffixPlaceholder + // ) + ProfileSuffixPlaceholder = "" +) + +// ApplicationService defines the interface for an edgex Application Service +type ApplicationService interface { + // AddRoute a custom REST route to the application service's internal webserver + // A reference to this ApplicationService is add the the context that is passed to the handler, which + // can be retrieved using the `AppService` key + AddRoute(route string, handler func(http.ResponseWriter, *http.Request), methods ...string) error + // ApplicationSettings returns the key/value map of custom settings + ApplicationSettings() map[string]string + // GetAppSettingStrings is a convenience function that parses the value for the specified custom + // application setting as a comma separated list. It returns the list of strings. + // An error is returned if the specified setting is no found. + GetAppSettingStrings(setting string) ([]string, error) + // SetFunctionsPipeline set the functions pipeline with the specified list of Application Functions. + // Note that the functions are executed in the order provided in the list. + // An error is returned if the list is empty. + SetFunctionsPipeline(transforms ...AppFunction) error + // MakeItRun starts the configured trigger to allow the functions pipeline to execute when the trigger + // receives data and starts the internal webserver. This is a long running function which does not return until + // the service is stopped or MakeItStop() is called. + // An error is returned if the trigger can not be create or initialized or if the internal webserver + // encounters an error. + MakeItRun() error + // MakeItStop stops the configured trigger so that the functions pipeline no longer executes. + // An error is returned + MakeItStop() + // RegisterCustomTriggerFactory registers a trigger factory for a custom trigger to be used. + RegisterCustomTriggerFactory(name string, factory func(TriggerConfig) (Trigger, error)) error + // Adds and returns a BackgroundPublisher which is used to publish asynchronously to the Edgex MessageBus. + // Not valid for use with the HTTP or External MQTT triggers + AddBackgroundPublisher(capacity int) BackgroundPublisher + // GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. + // An error is returned if the path is not found or any of the keys (if specified) are not found. + // Omit keys if all secret data for the specified path is required. + GetSecret(path string, keys ...string) (map[string]string, error) + // StoreSecret stores the specified secret data into the secret store (secure only) for the specified path + // An error is returned if: + // - Specified secret data is empty + // - Not using the secure secret store, i.e. not valid with InsecureSecrets configuration + // - Secure secret provider is not properly initialized + // - Connection issues with Secret Store service. + StoreSecret(path string, secretData map[string]string) error // LoggingClient returns the Logger client + LoggingClient() logger.LoggingClient + // EventClient returns the Event client. Note if Core Data is not specified in the Clients configuration, + // this will return nil. + EventClient() coredata.EventClient + // CommandClient returns the Command client. Note if Support Command is not specified in the Clients configuration, + // this will return nil. + CommandClient() command.CommandClient + // NotificationsClient returns the Notifications client. Note if Support Notifications is not specified in the + // Clients configuration, this will return nil. + NotificationsClient() notifications.NotificationsClient + // RegistryClient() returns the Registry client. Note the registry must been enable, otherwise this will return nil. + // Useful if service needs to add additional health checks or needs to get endpoint of another registered service + RegistryClient() registry.Client + // LoadConfigurablePipeline loads the function pipeline from configuration. + // An error is returned if the configuration is not valid, i.e. missing required function parameters, + // invalid function name, etc. + // Only useful if pipeline from configuration is always defined in configuration as in App Service Configurable. + LoadConfigurablePipeline() ([]AppFunction, error) +} diff --git a/appsdk/trigger.go b/pkg/interfaces/trigger.go similarity index 64% rename from appsdk/trigger.go rename to pkg/interfaces/trigger.go index a3819300a..f6e2a7653 100644 --- a/appsdk/trigger.go +++ b/pkg/interfaces/trigger.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,36 +14,33 @@ // limitations under the License. // -package appsdk +package interfaces import ( "context" "sync" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" ) -// Trigger interface provides an abstract means to pass messages through the function pipeline +// TriggerConfig provides a container to pass context needed for user defined triggers +type TriggerConfig struct { + Config types.MessageBusConfig + Logger logger.LoggingClient + ContextBuilder TriggerContextBuilder + MessageProcessor TriggerMessageProcessor +} + +// Trigger provides an abstract means to pass messages to the function pipeline type Trigger interface { // Initialize performs post creation initializations Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) } // TriggerMessageProcessor provides an interface that can be used by custom triggers to invoke the runtime -type TriggerMessageProcessor func(ctx *appcontext.Context, envelope types.MessageEnvelope) error +type TriggerMessageProcessor func(ctx AppFunctionContext, envelope types.MessageEnvelope) error -// TriggerContextBuilder provides an interface to construct an appcontext.Context for message -type TriggerContextBuilder func(env types.MessageEnvelope) *appcontext.Context - -// TriggerConfig provides a container to pass context needed to user defined triggers -type TriggerConfig struct { - Config *common.ConfigurationStruct - Logger logger.LoggingClient - ContextBuilder TriggerContextBuilder - MessageProcessor TriggerMessageProcessor -} +// TriggerContextBuilder provides an interface to construct an AppFunctionContext for message +type TriggerContextBuilder func(env types.MessageEnvelope) AppFunctionContext diff --git a/pkg/secure/mqttfactory.go b/pkg/secure/mqttfactory.go index 52e7db352..2c74de40a 100644 --- a/pkg/secure/mqttfactory.go +++ b/pkg/secure/mqttfactory.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,8 +22,9 @@ import ( "errors" "github.com/eclipse/paho.mqtt.golang" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) type mqttSecrets struct { @@ -48,18 +49,17 @@ const ( ) type MqttFactory struct { + appContext interfaces.AppFunctionContext logger logger.LoggingClient - secretProvider interfaces.SecretProvider authMode string secretPath string opts *mqtt.ClientOptions skipCertVerify bool } -func NewMqttFactory(lc logger.LoggingClient, sp interfaces.SecretProvider, mode string, path string, skipVerify bool) MqttFactory { +func NewMqttFactory(appContext interfaces.AppFunctionContext, mode string, path string, skipVerify bool) MqttFactory { return MqttFactory{ - logger: lc, - secretProvider: sp, + appContext: appContext, authMode: mode, secretPath: path, skipCertVerify: skipVerify, @@ -101,7 +101,7 @@ func (factory MqttFactory) getSecrets() (*mqttSecrets, error) { return nil, nil } - secrets, err := factory.secretProvider.GetSecrets(factory.secretPath) + secrets, err := factory.appContext.GetSecret(factory.secretPath) if err != nil { return nil, err } diff --git a/pkg/secure/mqttfactory_test.go b/pkg/secure/mqttfactory_test.go index 06fad0c41..dadc68ee4 100644 --- a/pkg/secure/mqttfactory_test.go +++ b/pkg/secure/mqttfactory_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,13 +21,18 @@ package secure import ( "errors" + "os" "testing" "github.com/eclipse/paho.mqtt.golang" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" ) const testCACert = `-----BEGIN CERTIFICATE----- @@ -102,8 +107,25 @@ CR6KVnoNdMwJZM3ARpBYNlhFTzDyew2WYLitZsN/uV8t+XxJFDyJQA== -----END RSA PRIVATE KEY----- ` +var lc logger.LoggingClient +var dic *di.Container +var context *appfunction.Context + +func TestMain(m *testing.M) { + lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + }) + + context = appfunction.NewContext("123", dic, "") + + os.Exit(m.Run()) +} + func TestValidateSecrets(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) tests := []struct { Name string AuthMode string @@ -162,12 +184,19 @@ func TestGetSecrets(t *testing.T) { "username": "TEST_USER", "password": "TEST_PASS", } + mockSecretProvider := &mocks.SecretProvider{} mockSecretProvider.On("GetSecrets", "").Return(nil) mockSecretProvider.On("GetSecrets", "/notfound").Return(nil, errors.New("Not Found")) mockSecretProvider.On("GetSecrets", "/mqtt").Return(expectedMqttSecrets, nil) - target := NewMqttFactory(logger.NewMockClient(), mockSecretProvider, "", "", false) + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + target := NewMqttFactory(context, "", "", false) tests := []struct { Name string AuthMode string @@ -201,7 +230,7 @@ func TestGetSecrets(t *testing.T) { } func TestConfigureMQTTClientForAuth(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() tests := []struct { Name string @@ -229,7 +258,7 @@ func TestConfigureMQTTClientForAuth(t *testing.T) { } } func TestConfigureMQTTClientForAuthWithUsernamePassword(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeUsernamePassword err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -244,7 +273,7 @@ func TestConfigureMQTTClientForAuthWithUsernamePassword(t *testing.T) { } func TestConfigureMQTTClientForAuthWithUsernamePasswordAndCA(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeUsernamePassword err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -260,7 +289,7 @@ func TestConfigureMQTTClientForAuthWithUsernamePasswordAndCA(t *testing.T) { } func TestConfigureMQTTClientForAuthWithCACert(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCA err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -276,7 +305,7 @@ func TestConfigureMQTTClientForAuthWithCACert(t *testing.T) { assert.Nil(t, target.opts.TLSConfig.Certificates) } func TestConfigureMQTTClientForAuthWithClientCert(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCert err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -294,7 +323,7 @@ func TestConfigureMQTTClientForAuthWithClientCert(t *testing.T) { } func TestConfigureMQTTClientForAuthWithClientCertNoCA(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCert err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -311,7 +340,7 @@ func TestConfigureMQTTClientForAuthWithClientCertNoCA(t *testing.T) { assert.Nil(t, target.opts.TLSConfig.ClientCAs) } func TestConfigureMQTTClientForAuthWithNone(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeNone err := target.configureMQTTClientForAuth(mqttSecrets{}) diff --git a/pkg/transforms/batch.go b/pkg/transforms/batch.go index 0f779596f..0434ff80a 100644 --- a/pkg/transforms/batch.go +++ b/pkg/transforms/batch.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import ( "sync" "time" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -75,7 +75,7 @@ type BatchConfig struct { batchThreshold int batchMode BatchMode batchData atomicBatchData - continuedPipelineTransforms []appcontext.AppFunction + continuedPipelineTransforms []interfaces.AppFunction timerActive common.AtomicBool done chan bool } @@ -124,19 +124,19 @@ func NewBatchByTimeAndCount(timeInterval string, batchThreshold int) (*BatchConf } // Batch ... -func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (batch *BatchConfig) Batch(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Batching Data") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Batching Data") + byteData, err := util.CoerceType(data) if err != nil { return false, err } // always append data - batch.batchData.append(data) + batch.batchData.append(byteData) // If its time only or time and count if batch.batchMode != BatchByCountOnly { @@ -145,9 +145,9 @@ func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...inte for { select { case <-batch.done: - edgexcontext.LoggingClient.Debug("Batch count has been reached") + ctx.LoggingClient().Debug("Batch count has been reached") case <-time.After(batch.parsedDuration): - edgexcontext.LoggingClient.Debug("Timer has elapsed") + ctx.LoggingClient().Debug("Timer has elapsed") } break } @@ -174,12 +174,12 @@ func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...inte } } - edgexcontext.LoggingClient.Debug("Forwarding Batched Data...") + ctx.LoggingClient().Debug("Forwarding Batched Data...") // we've met the threshold, lets clear out the buffer and send it forward in the pipeline if batch.batchData.length() > 0 { - copy := batch.batchData.all() + copyOfData := batch.batchData.all() batch.batchData.removeAll() - return true, copy + return true, copyOfData } return false, nil } diff --git a/pkg/transforms/batch_test.go b/pkg/transforms/batch_test.go index 2bd81818d..2c5a7478e 100644 --- a/pkg/transforms/batch_test.go +++ b/pkg/transforms/batch_test.go @@ -1,3 +1,19 @@ +// +// Copyright (c) 2021 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 transforms import ( @@ -13,7 +29,7 @@ var dataToBatch = [3]string{"Test1", "Test2", "Test3"} func TestBatchNoData(t *testing.T) { bs, _ := NewBatchByCount(1) - continuePipeline, err := bs.Batch(context) + continuePipeline, err := bs.Batch(context, nil) assert.False(t, continuePipeline) assert.Equal(t, "No Data Received", err.(error).Error()) diff --git a/pkg/transforms/compression.go b/pkg/transforms/compression.go index 6494b7fd6..8099c096f 100644 --- a/pkg/transforms/compression.go +++ b/pkg/transforms/compression.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,9 +23,11 @@ import ( "compress/zlib" "encoding/base64" "errors" + "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" ) @@ -39,15 +41,15 @@ func NewCompression() Compression { return Compression{} } -// CompressWithGZIP compresses data received as either a string,[]byte, or json.Marshaler using gzip algorithm +// CompressWithGZIP compresses data received as either a string,[]byte, or json.Marshaller using gzip algorithm // and returns a base64 encoded string as a []byte. -func (compression *Compression) CompressWithGZIP(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (compression *Compression) CompressWithGZIP(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Compression with GZIP") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Compression with GZIP") + rawData, err := util.CoerceType(data) if err != nil { return false, err } @@ -59,25 +61,32 @@ func (compression *Compression) CompressWithGZIP(edgexcontext *appcontext.Contex compression.gzipWriter.Reset(&buf) } - compression.gzipWriter.Write([]byte(data)) - compression.gzipWriter.Close() + _, err = compression.gzipWriter.Write(rawData) + if err != nil { + return false, fmt.Errorf("unable to write GZIP data") + } + + err = compression.gzipWriter.Close() + if err != nil { + return false, fmt.Errorf("unable to close GZIP data") + } // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, bytesBufferToBase64(buf) } -// CompressWithZLIB compresses data received as either a string,[]byte, or json.Marshaler using zlib algorithm +// CompressWithZLIB compresses data received as either a string,[]byte, or json.Marshaller using zlib algorithm // and returns a base64 encoded string as a []byte. -func (compression *Compression) CompressWithZLIB(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (compression *Compression) CompressWithZLIB(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Compression with ZLIB") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Compression with ZLIB") + byteData, err := util.CoerceType(data) if err != nil { return false, err } @@ -89,11 +98,18 @@ func (compression *Compression) CompressWithZLIB(edgexcontext *appcontext.Contex compression.zlibWriter.Reset(&buf) } - compression.zlibWriter.Write([]byte(data)) - compression.zlibWriter.Close() + _, err = compression.zlibWriter.Write(byteData) + if err != nil { + return false, fmt.Errorf("unable to write ZLIB data") + } + + err = compression.zlibWriter.Close() + if err != nil { + return false, fmt.Errorf("unable to close ZLIB data") + } // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, bytesBufferToBase64(buf) diff --git a/pkg/transforms/compression_test.go b/pkg/transforms/compression_test.go index 6010d8895..941da2657 100644 --- a/pkg/transforms/compression_test.go +++ b/pkg/transforms/compression_test.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,8 +33,6 @@ import ( const ( clearString = "This is the test string used for testing" - gzipString = "H4sIAAAJbogA/wrJyCxWyCxWKMlIVShJLS5RKC4pysxLVygtTk1RSMsvAgtm5qUDAgAA//8tdaMdKAAAAA==" - zlibString = "eJwKycgsVsgsVijJSFUoSS0uUSguKcrMS1coLU5NUUjLLwILZualAwIAAP//KucO4w==" ) func TestGzip(t *testing.T) { @@ -59,7 +57,7 @@ func TestGzip(t *testing.T) { continuePipeline2, result2 := comp.CompressWithGZIP(context, []byte(clearString)) assert.True(t, continuePipeline2) assert.Equal(t, result.([]byte), result2.([]byte)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestZlib(t *testing.T) { @@ -85,7 +83,7 @@ func TestZlib(t *testing.T) { continuePipeline2, result2 := comp.CompressWithZLIB(context, []byte(clearString)) assert.True(t, continuePipeline2) assert.Equal(t, result.([]byte), result2.([]byte)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } var result []byte diff --git a/pkg/transforms/conversion.go b/pkg/transforms/conversion.go index 1aef2923e..2294c1c2d 100755 --- a/pkg/transforms/conversion.go +++ b/pkg/transforms/conversion.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import ( "errors" "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) // Conversion houses various built in conversion transforms (XML, JSON, CSV) @@ -38,35 +38,38 @@ func NewConversion() Conversion { // TransformToXML transforms an EdgeX event to XML. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Conversion) TransformToXML(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, stringType interface{}) { - if len(params) < 1 { +func (f Conversion) TransformToXML(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, stringType interface{}) { + if data == nil { return false, errors.New("No Event Received") } - edgexcontext.LoggingClient.Debug("Transforming to XML") - if event, ok := params[0].(dtos.Event); ok { + + ctx.LoggingClient().Debug("Transforming to XML") + if event, ok := data.(dtos.Event); ok { xml, err := event.ToXML() if err != nil { return false, fmt.Errorf("unable to marshal Event to XML: %s", err.Error()) } - edgexcontext.ResponseContentType = clients.ContentTypeXML + + ctx.SetResponseContentType(clients.ContentTypeXML) return true, xml } + return false, errors.New("Unexpected type received") } // TransformToJSON transforms an EdgeX event to JSON. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Conversion) TransformToJSON(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, stringType interface{}) { - if len(params) < 1 { +func (f Conversion) TransformToJSON(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, stringType interface{}) { + if data == nil { return false, errors.New("No Event Received") } - edgexcontext.LoggingClient.Debug("Transforming to JSON") - if result, ok := params[0].(dtos.Event); ok { + ctx.LoggingClient().Debug("Transforming to JSON") + if result, ok := data.(dtos.Event); ok { b, err := json.Marshal(result) if err != nil { return false, errors.New("Error marshalling JSON") } - edgexcontext.ResponseContentType = clients.ContentTypeJSON + ctx.SetResponseContentType(clients.ContentTypeJSON) // should we return a byte[] or string? // return b return true, string(b) diff --git a/pkg/transforms/conversion_test.go b/pkg/transforms/conversion_test.go index 1a89a42d1..592c54867 100644 --- a/pkg/transforms/conversion_test.go +++ b/pkg/transforms/conversion_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -37,16 +37,18 @@ func TestTransformToXML(t *testing.T) { assert.NotNil(t, result) assert.True(t, continuePipeline) - assert.Equal(t, clients.ContentTypeXML, context.ResponseContentType) + assert.Equal(t, clients.ContentTypeXML, context.ResponseContentType()) assert.Equal(t, expectedResult, result.(string)) } -func TestTransformToXMLNoParameters(t *testing.T) { + +func TestTransformToXMLNoData(t *testing.T) { conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context) + continuePipeline, result := conv.TransformToXML(context, nil) assert.Equal(t, "No Event Received", result.(error).Error()) assert.False(t, continuePipeline) } + func TestTransformToXMLNotAnEvent(t *testing.T) { conv := NewConversion() continuePipeline, result := conv.TransformToXML(context, "") @@ -54,36 +56,6 @@ func TestTransformToXMLNotAnEvent(t *testing.T) { assert.Equal(t, "Unexpected type received", result.(error).Error()) assert.False(t, continuePipeline) -} -func TestTransformToXMLMultipleParametersValid(t *testing.T) { - // Event from device 1 - eventIn := dtos.Event{ - DeviceName: deviceName1, - } - expectedResult := `device100` - conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context, eventIn, "", "", "") - require.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) -} -func TestTransformToXMLMultipleParametersTwoEvents(t *testing.T) { - // Event from device 1 - eventIn1 := dtos.Event{ - DeviceName: deviceName1, - } - // Event from device 1 - eventIn2 := dtos.Event{ - DeviceName: deviceName2, - } - expectedResult := `device200` - conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context, eventIn2, eventIn1, "", "") - - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - } func TestTransformToJSON(t *testing.T) { @@ -97,52 +69,22 @@ func TestTransformToJSON(t *testing.T) { assert.NotNil(t, result) assert.True(t, continuePipeline) - assert.Equal(t, clients.ContentTypeJSON, context.ResponseContentType) + assert.Equal(t, clients.ContentTypeJSON, context.ResponseContentType()) assert.Equal(t, expectedResult, result.(string)) } + func TestTransformToJSONNoEvent(t *testing.T) { conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context) + continuePipeline, result := conv.TransformToJSON(context, nil) assert.Equal(t, "No Event Received", result.(error).Error()) assert.False(t, continuePipeline) } + func TestTransformToJSONNotAnEvent(t *testing.T) { conv := NewConversion() continuePipeline, result := conv.TransformToJSON(context, "") require.EqualError(t, result.(error), "Unexpected type received") assert.False(t, continuePipeline) - -} -func TestTransformToJSONMultipleParametersValid(t *testing.T) { - // Event from device 1 - eventIn := dtos.Event{ - DeviceName: deviceName1, - } - expectedResult := `{"apiVersion":"","id":"","deviceName":"device1","profileName":"","sourceName":"","origin":0,"readings":null}` - conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context, eventIn, "", "", "") - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - -} -func TestTransformToJSONMultipleParametersTwoEvents(t *testing.T) { - // Event from device 1 - eventIn1 := dtos.Event{ - DeviceName: deviceName1, - } - // Event from device 2 - eventIn2 := dtos.Event{ - DeviceName: deviceName2, - } - expectedResult := `{"apiVersion":"","id":"","deviceName":"device2","profileName":"","sourceName":"","origin":0,"readings":null}` - conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context, eventIn2, eventIn1, "", "") - - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - } diff --git a/pkg/transforms/coredata.go b/pkg/transforms/coredata.go index 2ec52ea7a..6cc3b2744 100644 --- a/pkg/transforms/coredata.go +++ b/pkg/transforms/coredata.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,7 +18,7 @@ package transforms import ( "errors" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -29,24 +29,26 @@ type CoreData struct { // NewCoreData Is provided to interact with CoreData func NewCoreData() *CoreData { - coredata := &CoreData{} - return coredata + coreData := &CoreData{} + return coreData } // PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in // CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. -func (cdc *CoreData) PushToCoreData(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { - // We didn't receive a result +func (cdc *CoreData) PushToCoreData(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { return false, errors.New("No Data Received") } - val, err := util.CoerceType(params[0]) + + val, err := util.CoerceType(data) if err != nil { return false, err } - result, err := edgexcontext.PushToCoreData(cdc.DeviceName, cdc.ReadingName, val) + + result, err := ctx.PushToCoreData(cdc.DeviceName, cdc.ReadingName, val) if err != nil { return false, err } + return true, result } diff --git a/pkg/transforms/coredata_test.go b/pkg/transforms/coredata_test.go index b6db6812f..bc69a7b3f 100644 --- a/pkg/transforms/coredata_test.go +++ b/pkg/transforms/coredata_test.go @@ -30,11 +30,12 @@ func TestPushToCore_ShouldFailPipelineOnError(t *testing.T) { assert.NotNil(t, result) assert.False(t, continuePipeline) } + func TestPushToCore_NoData(t *testing.T) { coreData := NewCoreData() coreData.DeviceName = "my-device" coreData.ReadingName = "my-device-resource" - continuePipeline, result := coreData.PushToCoreData(context) + continuePipeline, result := coreData.PushToCoreData(context, nil) assert.NotNil(t, result) assert.Equal(t, "No Data Received", result.(error).Error()) diff --git a/pkg/transforms/encryption.go b/pkg/transforms/encryption.go index 36a38ab9c..76774183e 100644 --- a/pkg/transforms/encryption.go +++ b/pkg/transforms/encryption.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -68,12 +68,14 @@ func pkcs5Padding(ciphertext []byte, blockSize int) []byte { // EncryptWithAES encrypts a string, []byte, or json.Marshaller type using AES encryption. // It will return a Base64 encode []byte of the encrypted data. -func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (aesData Encryption) EncryptWithAES(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { return false, errors.New("no data received to encrypt") } - edgexcontext.LoggingClient.Debug("Encrypting with AES") - data, err := util.CoerceType(params[0]) + + ctx.LoggingClient().Debug("Encrypting with AES") + + byteData, err := util.CoerceType(data) if err != nil { return false, err } @@ -87,7 +89,7 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param if len(aesData.SecretPath) != 0 && len(aesData.SecretName) != 0 { // Note secrets are cached so this call doesn't result in unneeded calls to SecretStore Service and // the cache is invalidated when StoreSecrets is used. - secretData, err := edgexcontext.SecretProvider.GetSecrets(aesData.SecretPath, aesData.SecretName) + secretData, err := ctx.GetSecret(aesData.SecretPath, aesData.SecretName) if err != nil { return false, fmt.Errorf( "unable to retieve encryption key at secret path=%s and name=%s", @@ -100,7 +102,7 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param return false, fmt.Errorf("unable find encryption key in secret data for name=%s", aesData.SecretName) } - edgexcontext.LoggingClient.Debugf( + ctx.LoggingClient().Debugf( "Using encryption key from Secret Store at path=%s & name=%s", aesData.SecretPath, aesData.SecretName) @@ -122,14 +124,14 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param } ecb := cipher.NewCBCEncrypter(block, iv) - content := pkcs5Padding(data, block.BlockSize()) + content := pkcs5Padding(byteData, block.BlockSize()) encrypted := make([]byte, len(content)) ecb.CryptBlocks(encrypted, content) encodedData := []byte(base64.StdEncoding.EncodeToString(encrypted)) // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, encodedData } diff --git a/pkg/transforms/encryption_test.go b/pkg/transforms/encryption_test.go index a1cfdba03..ba331a959 100644 --- a/pkg/transforms/encryption_test.go +++ b/pkg/transforms/encryption_test.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,9 @@ import ( "encoding/base64" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/stretchr/testify/assert" @@ -82,7 +84,7 @@ func TestNewEncryption(t *testing.T) { decrypted := aesDecrypt(encrypted.([]byte), aesData) assert.Equal(t, plainString, string(decrypted)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestNewEncryptionWithSecrets(t *testing.T) { @@ -91,7 +93,12 @@ func TestNewEncryptionWithSecrets(t *testing.T) { mockSP := &mocks.SecretProvider{} mockSP.On("GetSecrets", secretPath, secretName).Return(map[string]string{secretName: key}, nil) - context.SecretProvider = mockSP + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSP + }, + }) enc := NewEncryptionWithSecrets(secretPath, secretName, aesData.InitVector) @@ -101,7 +108,7 @@ func TestNewEncryptionWithSecrets(t *testing.T) { decrypted := aesDecrypt(encrypted.([]byte), aesData) assert.Equal(t, plainString, string(decrypted)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestAESNoData(t *testing.T) { @@ -113,7 +120,7 @@ func TestAESNoData(t *testing.T) { enc := NewEncryption(aesData.Key, aesData.InitVector) - continuePipeline, result := enc.EncryptWithAES(context) + continuePipeline, result := enc.EncryptWithAES(context, nil) assert.False(t, continuePipeline) assert.Error(t, result.(error), "expect an error") } diff --git a/pkg/transforms/filter.go b/pkg/transforms/filter.go index 330f7d4e4..b31877eca 100755 --- a/pkg/transforms/filter.go +++ b/pkg/transforms/filter.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" ) @@ -47,13 +47,13 @@ func NewFilterOut(filterValues []string) Filter { // FilterByProfileName filters based on the specified Device Profile, aka Class of Device. // If FilterOut is false, it filters out those Events not associated with the specified Device Profile listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Device Profile listed in FilterValues. -func (f Filter) FilterByProfileName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterByProfileName", "ProfileName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByProfileName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterByProfileName", "ProfileName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("ProfileName", event.ProfileName, edgexcontext.LoggingClient) + ok := f.doEventFilter("ProfileName", event.ProfileName, ctx.LoggingClient()) if ok { return true, *event } @@ -65,13 +65,13 @@ func (f Filter) FilterByProfileName(edgexcontext *appcontext.Context, params ... // FilterByDeviceName filters based on the specified Device Names, aka Instance of a Device. // If FilterOut is false, it filters out those Events not associated with the specified Device Names listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Device Names listed in FilterValues. -func (f Filter) FilterByDeviceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterByDeviceName", "DeviceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByDeviceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterByDeviceName", "DeviceName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("DeviceName", event.DeviceName, edgexcontext.LoggingClient) + ok := f.doEventFilter("DeviceName", event.DeviceName, ctx.LoggingClient()) if ok { return true, *event } @@ -82,13 +82,13 @@ func (f Filter) FilterByDeviceName(edgexcontext *appcontext.Context, params ...i // FilterBySourceName filters based on the specified Source for the Event, aka resource or command name. // If FilterOut is false, it filters out those Events not associated with the specified Source listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Source listed in FilterValues. -func (f Filter) FilterBySourceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterBySourceName", "SourceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterBySourceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterBySourceName", "SourceName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("SourceName", event.SourceName, edgexcontext.LoggingClient) + ok := f.doEventFilter("SourceName", event.SourceName, ctx.LoggingClient()) if ok { return true, *event } @@ -100,8 +100,8 @@ func (f Filter) FilterBySourceName(edgexcontext *appcontext.Context, params ...i // If FilterOut is false, it filters out those Event Readings not associated with the specified Resource Names listed in FilterValues. // If FilterOut is true, it out those Event Readings that are associated with the specified Resource Names listed in FilterValues. // This function will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - existingEvent, err := f.setupForFiltering("FilterByResourceName", "ResourceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByResourceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + existingEvent, err := f.setupForFiltering("FilterByResourceName", "ResourceName", ctx.LoggingClient(), data) if err != nil { return false, err } @@ -129,10 +129,10 @@ func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params .. } if !readingFilteredOut { - edgexcontext.LoggingClient.Debugf("Reading accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading accepted: %s", reading.ResourceName) auxEvent.Readings = append(auxEvent.Readings, reading) } else { - edgexcontext.LoggingClient.Debugf("Reading not accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading not accepted: %s", reading.ResourceName) } } } else { @@ -146,35 +146,35 @@ func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params .. } if readingFilteredFor { - edgexcontext.LoggingClient.Debugf("Reading accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading accepted: %s", reading.ResourceName) auxEvent.Readings = append(auxEvent.Readings, reading) } else { - edgexcontext.LoggingClient.Debugf("Reading not accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading not accepted: %s", reading.ResourceName) } } } if len(auxEvent.Readings) > 0 { - edgexcontext.LoggingClient.Debugf("Event accepted: %d remaining reading(s)", len(auxEvent.Readings)) + ctx.LoggingClient().Debugf("Event accepted: %d remaining reading(s)", len(auxEvent.Readings)) return true, auxEvent } - edgexcontext.LoggingClient.Debug("Event not accepted: 0 remaining readings") + ctx.LoggingClient().Debug("Event not accepted: 0 remaining readings") return false, nil } -func (f Filter) setupForFiltering(funcName string, filterProperty string, lc logger.LoggingClient, params ...interface{}) (*dtos.Event, error) { +func (f Filter) setupForFiltering(funcName string, filterProperty string, lc logger.LoggingClient, data interface{}) (*dtos.Event, error) { mode := "For" if f.FilterOut { mode = "Out" } lc.Debugf("Filtering %s by %s. FilterValues are: '[%v]'", mode, filterProperty, f.FilterValues) - if len(params) < 1 { + if data == nil { return nil, fmt.Errorf("%s: no Event Received", funcName) } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return nil, fmt.Errorf("%s: type received is not an Event", funcName) } diff --git a/pkg/transforms/filter_test.go b/pkg/transforms/filter_test.go index a0270cd74..2cc7d76bb 100644 --- a/pkg/transforms/filter_test.go +++ b/pkg/transforms/filter_test.go @@ -50,19 +50,18 @@ func TestFilter_FilterByProfileName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParam bool }{ - {"filter for - no event", []string{profileName1}, true, nil, true, false}, - {"filter for - no filter values", []string{}, false, &profile1Event, false, false}, - {"filter for with extra params - found", []string{profileName1}, false, &profile1Event, false, true}, - {"filter for - found", []string{profileName1}, false, &profile1Event, false, false}, - {"filter for - not found", []string{profileName2}, false, &profile1Event, true, false}, - - {"filter out - no event", []string{profileName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &profile1Event, false, false}, - {"filter out extra param - found", []string{profileName1}, true, &profile1Event, true, true}, - {"filter out - found", []string{profileName1}, true, &profile1Event, true, false}, - {"filter out - not found", []string{profileName2}, true, &profile1Event, false, false}, + {"filter for - no event", []string{profileName1}, true, nil, true}, + {"filter for - no filter values", []string{}, false, &profile1Event, false}, + {"filter for with extra data - found", []string{profileName1}, false, &profile1Event, false}, + {"filter for - found", []string{profileName1}, false, &profile1Event, false}, + {"filter for - not found", []string{profileName2}, false, &profile1Event, true}, + + {"filter out - no event", []string{profileName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &profile1Event, false}, + {"filter out extra param - found", []string{profileName1}, true, &profile1Event, true}, + {"filter out - found", []string{profileName1}, true, &profile1Event, true}, + {"filter out - not found", []string{profileName2}, true, &profile1Event, false}, } for _, test := range tests { @@ -77,17 +76,11 @@ func TestFilter_FilterByProfileName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByProfileName(context) + continuePipeline, result := filter.FilterByProfileName(context, nil) assert.EqualError(t, result.(error), "FilterByProfileName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParam { - continuePipeline, result = filter.FilterByProfileName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByProfileName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByProfileName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -107,19 +100,18 @@ func TestFilter_FilterByDeviceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParams bool }{ - {"filter for - no event", []string{deviceName1}, false, nil, true, false}, - {"filter for - no filter values", []string{}, false, &device1Event, false, false}, - {"filter for with extra params - found", []string{deviceName1}, false, &device1Event, false, true}, - {"filter for - found", []string{deviceName1}, false, &device1Event, false, false}, - {"filter for - not found", []string{deviceName2}, false, &device1Event, true, false}, - - {"filter out - no event", []string{deviceName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &device1Event, false, false}, - {"filter out extra param - found", []string{deviceName1}, true, &device1Event, true, true}, - {"filter out - found", []string{deviceName1}, true, &device1Event, true, false}, - {"filter out - not found", []string{deviceName2}, true, &device1Event, false, false}, + {"filter for - no event", []string{deviceName1}, false, nil, true}, + {"filter for - no filter values", []string{}, false, &device1Event, false}, + {"filter for with extra data - found", []string{deviceName1}, false, &device1Event, false}, + {"filter for - found", []string{deviceName1}, false, &device1Event, false}, + {"filter for - not found", []string{deviceName2}, false, &device1Event, true}, + + {"filter out - no event", []string{deviceName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &device1Event, false}, + {"filter out extra param - found", []string{deviceName1}, true, &device1Event, true}, + {"filter out - found", []string{deviceName1}, true, &device1Event, true}, + {"filter out - not found", []string{deviceName2}, true, &device1Event, false}, } for _, test := range tests { @@ -134,17 +126,11 @@ func TestFilter_FilterByDeviceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByDeviceName(context) + continuePipeline, result := filter.FilterByDeviceName(context, nil) assert.EqualError(t, result.(error), "FilterByDeviceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParams { - continuePipeline, result = filter.FilterByDeviceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByDeviceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByDeviceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -164,19 +150,18 @@ func TestFilter_FilterBySourceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParam bool }{ - {"filter for - no event", []string{sourceName1}, true, nil, true, false}, - {"filter for - no filter values", []string{}, false, &source1Event, false, false}, - {"filter for with extra params - found", []string{sourceName1}, false, &source1Event, false, true}, - {"filter for - found", []string{sourceName1}, false, &source1Event, false, false}, - {"filter for - not found", []string{sourceName2}, false, &source1Event, true, false}, - - {"filter out - no event", []string{sourceName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &source1Event, false, false}, - {"filter out extra param - found", []string{sourceName1}, true, &source1Event, true, true}, - {"filter out - found", []string{sourceName1}, true, &source1Event, true, false}, - {"filter out - not found", []string{sourceName2}, true, &source1Event, false, false}, + {"filter for - no event", []string{sourceName1}, true, nil, true}, + {"filter for - no filter values", []string{}, false, &source1Event, false}, + {"filter for with extra data - found", []string{sourceName1}, false, &source1Event, false}, + {"filter for - found", []string{sourceName1}, false, &source1Event, false}, + {"filter for - not found", []string{sourceName2}, false, &source1Event, true}, + + {"filter out - no event", []string{sourceName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &source1Event, false}, + {"filter out extra param - found", []string{sourceName1}, true, &source1Event, true}, + {"filter out - found", []string{sourceName1}, true, &source1Event, true}, + {"filter out - not found", []string{sourceName2}, true, &source1Event, false}, } for _, test := range tests { @@ -191,17 +176,11 @@ func TestFilter_FilterBySourceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterBySourceName(context) + continuePipeline, result := filter.FilterBySourceName(context, nil) assert.EqualError(t, result.(error), "FilterBySourceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParam { - continuePipeline, result = filter.FilterBySourceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterBySourceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterBySourceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -241,29 +220,28 @@ func TestFilter_FilterByResourceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParams bool ExpectedReadingCount int }{ - {"filter for - no event", []string{resource1}, false, nil, true, false, 0}, - {"filter for extra param - found", []string{resource1}, false, &resource1Event, false, true, 1}, - {"filter for 0 in R1 - no change", []string{}, false, &resource1Event, false, false, 1}, - {"filter for 1 in R1 - 1 of 1 found", []string{resource1}, false, &resource1Event, false, false, 1}, - {"filter for 1 in 2R - 1 of 2 found", []string{resource1}, false, &twoResourceEvent, false, false, 1}, - {"filter for 2 in R1 - 1 of 1 found", []string{resource1, resource2}, false, &resource1Event, false, false, 1}, - {"filter for 2 in 2R - 2 of 2 found", []string{resource1, resource2}, false, &twoResourceEvent, false, false, 2}, - {"filter for 2 in R2 - 1 of 2 found", []string{resource1, resource2}, false, &resource2Event, false, false, 1}, - {"filter for 1 in R2 - not found", []string{resource1}, false, &resource2Event, true, false, 0}, - - {"filter out - no event", []string{resource1}, true, nil, true, false, 0}, - {"filter out extra param - found", []string{resource1}, true, &resource1Event, true, true, 0}, - {"filter out 0 in R1 - no change", []string{}, true, &resource1Event, false, false, 1}, - {"filter out 1 in R1 - 1 of 1 found", []string{resource1}, true, &resource1Event, true, false, 0}, - {"filter out 1 in R2 - not found", []string{resource1}, true, &resource2Event, false, false, 1}, - {"filter out 1 in 2R - 1 of 2 found", []string{resource1}, true, &twoResourceEvent, false, false, 1}, - {"filter out 2 in R1 - 1 of 1 found", []string{resource1, resource2}, true, &resource1Event, true, false, 0}, - {"filter out 2 in R2 - 1 of 1 found", []string{resource1, resource2}, true, &resource2Event, true, false, 0}, - {"filter out 2 in 2R - 2 of 2 found", []string{resource1, resource2}, true, &twoResourceEvent, true, false, 0}, - {"filter out 2 in R3 - not found", []string{resource1, resource2}, true, &resource3Event, false, false, 1}, + {"filter for - no event", []string{resource1}, false, nil, true, 0}, + {"filter for extra param - found", []string{resource1}, false, &resource1Event, false, 1}, + {"filter for 0 in R1 - no change", []string{}, false, &resource1Event, false, 1}, + {"filter for 1 in R1 - 1 of 1 found", []string{resource1}, false, &resource1Event, false, 1}, + {"filter for 1 in 2R - 1 of 2 found", []string{resource1}, false, &twoResourceEvent, false, 1}, + {"filter for 2 in R1 - 1 of 1 found", []string{resource1, resource2}, false, &resource1Event, false, 1}, + {"filter for 2 in 2R - 2 of 2 found", []string{resource1, resource2}, false, &twoResourceEvent, false, 2}, + {"filter for 2 in R2 - 1 of 2 found", []string{resource1, resource2}, false, &resource2Event, false, 1}, + {"filter for 1 in R2 - not found", []string{resource1}, false, &resource2Event, true, 0}, + + {"filter out - no event", []string{resource1}, true, nil, true, 0}, + {"filter out extra param - found", []string{resource1}, true, &resource1Event, true, 0}, + {"filter out 0 in R1 - no change", []string{}, true, &resource1Event, false, 1}, + {"filter out 1 in R1 - 1 of 1 found", []string{resource1}, true, &resource1Event, true, 0}, + {"filter out 1 in R2 - not found", []string{resource1}, true, &resource2Event, false, 1}, + {"filter out 1 in 2R - 1 of 2 found", []string{resource1}, true, &twoResourceEvent, false, 1}, + {"filter out 2 in R1 - 1 of 1 found", []string{resource1, resource2}, true, &resource1Event, true, 0}, + {"filter out 2 in R2 - 1 of 1 found", []string{resource1, resource2}, true, &resource2Event, true, 0}, + {"filter out 2 in 2R - 2 of 2 found", []string{resource1, resource2}, true, &twoResourceEvent, true, 0}, + {"filter out 2 in R3 - not found", []string{resource1, resource2}, true, &resource3Event, false, 1}, } for _, test := range tests { @@ -278,17 +256,11 @@ func TestFilter_FilterByResourceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByResourceName(context) + continuePipeline, result := filter.FilterByResourceName(context, nil) assert.EqualError(t, result.(error), "FilterByResourceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParams { - continuePipeline, result = filter.FilterByResourceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByResourceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByResourceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil { diff --git a/pkg/transforms/http.go b/pkg/transforms/http.go index 320b37884..5d90f872b 100644 --- a/pkg/transforms/http.go +++ b/pkg/transforms/http.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,7 +23,7 @@ import ( "io/ioutil" "net/http" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" @@ -63,21 +63,21 @@ func NewHTTPSenderWithSecretHeader(url string, mimeType string, persistOnError b // HTTPPost will send data from the previous function to the specified Endpoint via http POST. // If no previous function exists, then the event that triggered the pipeline will be used. // An empty string for the mimetype will default to application/json. -func (sender HTTPSender) HTTPPost(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return sender.httpSend(edgexcontext, params, http.MethodPost) +func (sender HTTPSender) HTTPPost(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return sender.httpSend(ctx, data, http.MethodPost) } // HTTPPut will send data from the previous function to the specified Endpoint via http PUT. // If no previous function exists, then the event that triggered the pipeline will be used. // An empty string for the mimetype will default to application/json. -func (sender HTTPSender) HTTPPut(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return sender.httpSend(edgexcontext, params, http.MethodPut) +func (sender HTTPSender) HTTPPut(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return sender.httpSend(ctx, data, http.MethodPut) } -func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []interface{}, method string) (bool, interface{}) { - lc := edgexcontext.LoggingClient +func (sender HTTPSender) httpSend(ctx interfaces.AppFunctionContext, data interface{}, method string) (bool, interface{}) { + lc := ctx.LoggingClient() - if len(params) < 1 { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } @@ -86,7 +86,7 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int sender.MimeType = "application/json" } - exportData, err := util.CoerceType(params[0]) + exportData, err := util.CoerceType(data) if err != nil { return false, err } @@ -103,7 +103,7 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int } var theSecrets map[string]string if usingSecrets { - theSecrets, err = edgexcontext.GetSecrets(sender.SecretPath, sender.SecretName) + theSecrets, err = ctx.GetSecret(sender.SecretPath, sender.SecretName) if err != nil { return false, err } @@ -118,26 +118,26 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int req.Header.Set("Content-Type", sender.MimeType) - edgexcontext.LoggingClient.Debug("POSTing data") + ctx.LoggingClient().Debug("POSTing data") response, err := client.Do(req) if err != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, err } defer func() { _ = response.Body.Close() }() - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Response: %s", response.Status)) - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Sent data: %s", string(exportData))) + ctx.LoggingClient().Debug(fmt.Sprintf("Response: %s", response.Status)) + ctx.LoggingClient().Debug(fmt.Sprintf("Sent data: %s", string(exportData))) bodyBytes, errReadingBody := ioutil.ReadAll(response.Body) if errReadingBody != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, errReadingBody } - edgexcontext.LoggingClient.Trace("Data exported", "Transport", "HTTP", clients.CorrelationHeader, edgexcontext.CorrelationID) + ctx.LoggingClient().Trace("Data exported", "Transport", "HTTP", clients.CorrelationHeader, ctx.CorrelationID) // continues the pipeline if we get a 2xx response, stops pipeline if non-2xx response if response.StatusCode < 200 || response.StatusCode >= 300 { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, fmt.Errorf("export failed with %d HTTP status code", response.StatusCode) } @@ -170,8 +170,8 @@ func (sender HTTPSender) determineIfUsingSecrets() (bool, error) { return true, nil } -func (sender HTTPSender) setRetryData(ctx *appcontext.Context, exportData []byte) { +func (sender HTTPSender) setRetryData(ctx interfaces.AppFunctionContext, exportData []byte) { if sender.PersistOnError { - ctx.RetryData = exportData + ctx.SetRetryData(exportData) } } diff --git a/pkg/transforms/http_test.go b/pkg/transforms/http_test.go index 286f59df2..c217a5787 100644 --- a/pkg/transforms/http_test.go +++ b/pkg/transforms/http_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,16 +25,10 @@ import ( "strings" "testing" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + mocks2 "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,28 +39,7 @@ const ( badPath = "/some-path/bad" ) -var lc logger.LoggingClient -var config *common.ConfigurationStruct -var context *appcontext.Context - -func TestMain(m *testing.M) { - lc = logger.NewMockClient() - eventClient := coredata.NewEventClient(local.New("http://test" + clients.ApiEventRoute)) - - config = &common.ConfigurationStruct{} - - context = &appcontext.Context{ - LoggingClient: lc, - EventClient: eventClient, - Configuration: config, - } - - m.Run() -} - func TestHTTPPostPut(t *testing.T) { - context.CorrelationID = "123" - var methodUsed string handler := func(w http.ResponseWriter, r *http.Request) { @@ -118,7 +91,7 @@ func TestHTTPPostPut(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) methodUsed = "" sender := NewHTTPSender(`http://`+targetUrl.Host+test.Path, "", test.PersistOnFail) @@ -128,7 +101,7 @@ func TestHTTPPostPut(t *testing.T) { sender.HTTPPut(context, msgStr) } - assert.Equal(t, test.RetryDataSet, context.RetryData != nil) + assert.Equal(t, test.RetryDataSet, context.RetryData() != nil) assert.Equal(t, test.ExpectedMethod, methodUsed) }) } @@ -139,10 +112,15 @@ func TestHTTPPostPutWithSecrets(t *testing.T) { expectedValue := "my-API-key" - mockSP := &mocks.SecretProvider{} + mockSP := &mocks2.SecretProvider{} mockSP.On("GetSecrets", "/path", "header").Return(map[string]string{"Secret-Header-Name": expectedValue}, nil) mockSP.On("GetSecrets", "/path", "bogus").Return(nil, errors.New("FAKE NOT FOUND ERROR")) - context.SecretProvider = mockSP + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSP + }, + }) // create test server with handler ts := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { @@ -221,7 +199,7 @@ func TestHTTPPostPutWithSecrets(t *testing.T) { func TestHTTPPostNoParameterPassed(t *testing.T) { sender := NewHTTPSender("", "", false) - continuePipeline, result := sender.HTTPPost(context) + continuePipeline, result := sender.HTTPPost(context, nil) assert.False(t, continuePipeline, "Pipeline should stop") assert.Error(t, result.(error), "Result should be an error") @@ -230,7 +208,7 @@ func TestHTTPPostNoParameterPassed(t *testing.T) { func TestHTTPPutNoParameterPassed(t *testing.T) { sender := NewHTTPSender("", "", false) - continuePipeline, result := sender.HTTPPut(context) + continuePipeline, result := sender.HTTPPut(context, nil) assert.False(t, continuePipeline, "Pipeline should stop") assert.Error(t, result.(error), "Result should be an error") diff --git a/pkg/transforms/jsonlogic.go b/pkg/transforms/jsonlogic.go index ff6844de1..c09538748 100644 --- a/pkg/transforms/jsonlogic.go +++ b/pkg/transforms/jsonlogic.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,11 +20,13 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "strconv" "strings" "github.com/diegoholiveira/jsonlogic" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -41,29 +43,35 @@ func NewJSONLogic(rule string) JSONLogic { } // Evaluate ... -func (logic JSONLogic) Evaluate(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (logic JSONLogic) Evaluate(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - coercedData, err := util.CoerceType(params[0]) + coercedData, err := util.CoerceType(data) if err != nil { return false, err } - data := strings.NewReader(string(coercedData)) + reader := strings.NewReader(string(coercedData)) rule := strings.NewReader(logic.Rule) - var logicresult bytes.Buffer - edgexcontext.LoggingClient.Debug("Applying JSONLogic Rule") - err = jsonlogic.Apply(rule, data, &logicresult) + + var logicResult bytes.Buffer + ctx.LoggingClient().Debug("Applying JSONLogic Rule") + err = jsonlogic.Apply(rule, reader, &logicResult) if err != nil { - return false, err + return false, fmt.Errorf("unable to apply JSONLogic rule: %s", err.Error()) } + var result bool - decoder := json.NewDecoder(&logicresult) - decoder.Decode(&result) - edgexcontext.LoggingClient.Debug("Condition met: " + strconv.FormatBool(result)) + decoder := json.NewDecoder(&logicResult) + err = decoder.Decode(&result) + if err != nil { + return false, fmt.Errorf("unable to decode JSONLogic result: %s", err.Error()) + } + + ctx.LoggingClient().Debug("Condition met: " + strconv.FormatBool(result)) - return result, params[0] + return result, data } diff --git a/pkg/transforms/jsonlogic_test.go b/pkg/transforms/jsonlogic_test.go index 83afe2dad..fd7a5a64c 100644 --- a/pkg/transforms/jsonlogic_test.go +++ b/pkg/transforms/jsonlogic_test.go @@ -1,16 +1,33 @@ +// +// Copyright (c) 2021 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 transforms import ( + "fmt" "testing" - jlogic "github.com/diegoholiveira/jsonlogic" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJSONLogicSimple(t *testing.T) { - jsonlogic := NewJSONLogic(`{"==": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, "{}") + continuePipeline, result := jsonLogic.Evaluate(context, "{}") assert.NotNil(t, result) assert.True(t, continuePipeline) @@ -18,13 +35,13 @@ func TestJSONLogicSimple(t *testing.T) { } func TestJSONLogicAdvanced(t *testing.T) { - jsonlogic := NewJSONLogic(`{ "and" : [ + jsonLogic := NewJSONLogic(`{ "and" : [ {"<" : [ { "var" : "temp" }, 110 ]}, {"==" : [ { "var" : "sensor.type" }, "temperature" ] } ] }`) data := `{ "temp" : 100, "sensor" : { "type" : "temperature" } }` - continuePipeline, result := jsonlogic.Evaluate(context, data) + continuePipeline, result := jsonLogic.Evaluate(context, data) assert.NotNil(t, result) assert.True(t, continuePipeline) @@ -33,9 +50,9 @@ func TestJSONLogicAdvanced(t *testing.T) { func TestJSONLogicMalformedJSONRule(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"==: [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==: [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, `{}`) + continuePipeline, result := jsonLogic.Evaluate(context, `{}`) assert.NotNil(t, result) assert.False(t, continuePipeline) @@ -44,20 +61,21 @@ func TestJSONLogicMalformedJSONRule(t *testing.T) { func TestJSONLogicValidJSONBadRule(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"notanoperator": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"notAnOperator": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, `{}`) + continuePipeline, result := jsonLogic.Evaluate(context, `{}`) assert.NotNil(t, result) assert.False(t, continuePipeline) - assert.Equal(t, "The operator \"notanoperator\" is not supported", result.(jlogic.ErrInvalidOperator).Error()) + require.IsType(t, fmt.Errorf(""), result) + assert.Equal(t, "unable to apply JSONLogic rule: The operator \"notAnOperator\" is not supported", result.(error).Error()) } func TestJSONLogicNoData(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"notanoperator": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"notAnOperator": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context) + continuePipeline, result := jsonLogic.Evaluate(context, nil) assert.NotNil(t, result) assert.False(t, continuePipeline) @@ -66,9 +84,9 @@ func TestJSONLogicNoData(t *testing.T) { func TestJSONLogicNonJSONData(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"==": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, "iamnotjson") + continuePipeline, result := jsonLogic.Evaluate(context, "iAmNotJson") assert.NotNil(t, result) assert.False(t, continuePipeline) diff --git a/pkg/transforms/mqttsecret.go b/pkg/transforms/mqttsecret.go index 06f9a7167..7407d4b3f 100644 --- a/pkg/transforms/mqttsecret.go +++ b/pkg/transforms/mqttsecret.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import ( MQTT "github.com/eclipse/paho.mqtt.golang" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -83,23 +83,18 @@ func NewMQTTSecretSender(mqttConfig MQTTSecretConfig, persistOnError bool) *MQTT return sender } -func (sender *MQTTSecretSender) initializeMQTTClient(edgexcontext *appcontext.Context) error { +func (sender *MQTTSecretSender) initializeMQTTClient(ctx interfaces.AppFunctionContext) error { sender.lock.Lock() defer sender.lock.Unlock() // If the conditions changed while waiting for the lock, i.e. other thread completed the initialization, // then skip doing anything - if sender.client != nil && !sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.SecretsLastUpdated()) { + if sender.client != nil && !sender.secretsLastRetrieved.Before(ctx.SecretsLastUpdated()) { return nil } - mqttFactory := secure.NewMqttFactory( - edgexcontext.LoggingClient, - edgexcontext.SecretProvider, - sender.mqttConfig.AuthMode, - sender.mqttConfig.SecretPath, - sender.mqttConfig.SkipCertVerify, - ) + config := sender.mqttConfig + mqttFactory := secure.NewMqttFactory(ctx, config.AuthMode, config.SecretPath, config.SkipCertVerify) client, err := mqttFactory.Create(sender.opts) if err != nil { @@ -112,7 +107,7 @@ func (sender *MQTTSecretSender) initializeMQTTClient(edgexcontext *appcontext.Co return nil } -func (sender *MQTTSecretSender) connectToBroker(edgexcontext *appcontext.Context, exportData []byte) error { +func (sender *MQTTSecretSender) connectToBroker(ctx interfaces.AppFunctionContext, exportData []byte) error { sender.lock.Lock() defer sender.lock.Unlock() @@ -122,40 +117,40 @@ func (sender *MQTTSecretSender) connectToBroker(edgexcontext *appcontext.Context return nil } - edgexcontext.LoggingClient.Info("Connecting to mqtt server for export") + ctx.LoggingClient().Info("Connecting to mqtt server for export") if token := sender.client.Connect(); token.Wait() && token.Error() != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) subMessage := "dropping event" if sender.persistOnError { subMessage = "persisting Event for later retry" } return fmt.Errorf("Could not connect to mqtt server for export, %s. Error: %s", subMessage, token.Error().Error()) } - edgexcontext.LoggingClient.Info("Connected to mqtt server for export") + ctx.LoggingClient().Info("Connected to mqtt server for export") return nil } // MQTTSend sends data from the previous function to the specified MQTT broker. // If no previous function exists, then the event that triggered the pipeline will be used. -func (sender *MQTTSecretSender) MQTTSend(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (sender *MQTTSecretSender) MQTTSend(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - exportData, err := util.CoerceType(params[0]) + exportData, err := util.CoerceType(data) if err != nil { return false, err } // if we havent initialized the client yet OR the cache has been invalidated (due to new/updated secrets) we need to (re)initialize the client - if sender.client == nil || sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.SecretsLastUpdated()) { - err := sender.initializeMQTTClient(edgexcontext) + if sender.client == nil || sender.secretsLastRetrieved.Before(ctx.SecretsLastUpdated()) { + err := sender.initializeMQTTClient(ctx) if err != nil { return false, err } } if !sender.client.IsConnected() { - err := sender.connectToBroker(edgexcontext, exportData) + err := sender.connectToBroker(ctx, exportData) if err != nil { return false, err } @@ -164,18 +159,18 @@ func (sender *MQTTSecretSender) MQTTSend(edgexcontext *appcontext.Context, param token := sender.client.Publish(sender.mqttConfig.Topic, sender.mqttConfig.QoS, sender.mqttConfig.Retain, exportData) token.Wait() if token.Error() != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, token.Error() } - edgexcontext.LoggingClient.Debug("Sent data to MQTT Broker") - edgexcontext.LoggingClient.Trace("Data exported", "Transport", "MQTT", clients.CorrelationHeader, edgexcontext.CorrelationID) + ctx.LoggingClient().Debug("Sent data to MQTT Broker") + ctx.LoggingClient().Trace("Data exported", "Transport", "MQTT", clients.CorrelationHeader, ctx.CorrelationID) return true, nil } -func (sender *MQTTSecretSender) setRetryData(ctx *appcontext.Context, exportData []byte) { +func (sender *MQTTSecretSender) setRetryData(ctx interfaces.AppFunctionContext, exportData []byte) { if sender.persistOnError { - ctx.RetryData = exportData + ctx.SetRetryData(exportData) } } diff --git a/pkg/transforms/mqttsecret_broker_test.go b/pkg/transforms/mqttsecret_broker_test.go deleted file mode 100644 index 45feef653..000000000 --- a/pkg/transforms/mqttsecret_broker_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// +build brokerRunning -// -// 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. -// - -// This test will only be executed if the tag brokerRunning is added when running -// the tests with a command like: -// go test -tags brokerRunning - -package transforms - -import ( - "testing" - "time" - - MQTT "github.com/eclipse/paho.mqtt.golang" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" -) - -func TestMQTTSendWithData(t *testing.T) { - sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) - sender.mqttConfig = MQTTSecretConfig{ - SecretPath: "/mqtt", - } - sender.client = MQTT.NewClient(sender.opts) - mockSecretProvider := &mocks.SecretProvider{} - mockSecretProvider.On("SecretsLastUpdated").Return(time.Now()) - context.SecretProvider = mockSecretProvider - sender.MQTTSend(context, "sendme") - // require.True(t, continuePipeline) - // require.Error(t, result.(error)) -} diff --git a/pkg/transforms/mqttsecret_test.go b/pkg/transforms/mqttsecret_test.go index b30b047fb..952a48f83 100644 --- a/pkg/transforms/mqttsecret_test.go +++ b/pkg/transforms/mqttsecret_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,25 +27,25 @@ import ( ) func TestSetRetryDataPersistFalse(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) sender := NewMQTTSecretSender(MQTTSecretConfig{}, false) sender.mqttConfig = MQTTSecretConfig{} sender.setRetryData(context, []byte("data")) - assert.Nil(t, context.RetryData) + assert.Nil(t, context.RetryData()) } func TestSetRetryDataPersistTrue(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) sender.mqttConfig = MQTTSecretConfig{} sender.setRetryData(context, []byte("data")) - assert.Equal(t, []byte("data"), context.RetryData) + assert.Equal(t, []byte("data"), context.RetryData()) } -func TestMQTTSendNoParams(t *testing.T) { +func TestMQTTSendNodata(t *testing.T) { sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) sender.mqttConfig = MQTTSecretConfig{} - continuePipeline, result := sender.MQTTSend(context) + continuePipeline, result := sender.MQTTSend(context, nil) require.False(t, continuePipeline) require.Error(t, result.(error)) } diff --git a/pkg/transforms/outputdata.go b/pkg/transforms/responsedata.go similarity index 54% rename from pkg/transforms/outputdata.go rename to pkg/transforms/responsedata.go index 86a78721b..5109863aa 100644 --- a/pkg/transforms/outputdata.go +++ b/pkg/transforms/responsedata.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,43 +17,42 @@ package transforms import ( + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" ) -// OutputData houses transform for outputting data to configured trigger response, i.e. message bus -type OutputData struct { +// ResponseData houses transform for outputting data to configured trigger response, i.e. message bus +type ResponseData struct { ResponseContentType string } -// NewOutputData creates, initializes and returns a new instance of OutputData -func NewOutputData() OutputData { - return OutputData{} +// NewResponseData creates, initializes and returns a new instance of ResponseData +func NewResponseData() ResponseData { + return ResponseData{} } -// SetOutputData sets the output data to that passed in from the previous function. -// It will return an error and stop the pipeline if the input data is not of type []byte, string or json.Mashaler -func (f OutputData) SetOutputData(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { +// SetResponseData sets the response data to that passed in from the previous function. +// It will return an error and stop the pipeline if the input data is not of type []byte, string or json.Marshaller +func (f ResponseData) SetResponseData(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("Setting output data") + ctx.LoggingClient().Debug("Setting response data") - if len(params) < 1 { + if data == nil { // We didn't receive a result return false, nil } - data, err := util.CoerceType(params[0]) + byteData, err := util.CoerceType(data) if err != nil { return false, err } if len(f.ResponseContentType) > 0 { - edgexcontext.ResponseContentType = f.ResponseContentType + ctx.SetResponseContentType(f.ResponseContentType) } // By setting this the data will be posted back to to configured trigger response, i.e. message bus - edgexcontext.OutputData = data + ctx.SetResponseData(byteData) - return true, params[0] + return true, data } diff --git a/pkg/transforms/outputdata_test.go b/pkg/transforms/responsedata_test.go similarity index 57% rename from pkg/transforms/outputdata_test.go rename to pkg/transforms/responsedata_test.go index 83397e80a..728a5c0fc 100644 --- a/pkg/transforms/outputdata_test.go +++ b/pkg/transforms/responsedata_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,34 +26,34 @@ import ( "github.com/stretchr/testify/require" ) -func TestSetOutputDataString(t *testing.T) { +func TestSetResponseDataString(t *testing.T) { expected := `0id1000` - target := NewOutputData() + target := NewResponseData() - continuePipeline, result := target.SetOutputData(context, expected) + continuePipeline, result := target.SetResponseData(context, expected) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, expected, actual) } -func TestSetOutputDataBytes(t *testing.T) { +func TestSetResponseDataBytes(t *testing.T) { var expected []byte expected = []byte(`0id1000`) - target := NewOutputData() + target := NewResponseData() - continuePipeline, result := target.SetOutputData(context, expected) + continuePipeline, result := target.SetResponseData(context, expected) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, string(expected), actual) } -func TestSetOutputDataEvent(t *testing.T) { - target := NewOutputData() +func TestSetResponseDataEvent(t *testing.T) { + target := NewResponseData() eventIn := dtos.Event{ DeviceName: deviceName1, @@ -61,38 +61,26 @@ func TestSetOutputDataEvent(t *testing.T) { expected, _ := json.Marshal(eventIn) - continuePipeline, result := target.SetOutputData(context, eventIn) + continuePipeline, result := target.SetResponseData(context, eventIn) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, string(expected), actual) } -func TestSetOutputDataNoData(t *testing.T) { - target := NewOutputData() - continuePipeline, result := target.SetOutputData(context) +func TestSetResponseDataNoData(t *testing.T) { + target := NewResponseData() + continuePipeline, result := target.SetResponseData(context, nil) assert.Nil(t, result) assert.False(t, continuePipeline) } -func TestSetOutputDataMultipleParametersValid(t *testing.T) { - expected := `0id1000` - target := NewOutputData() - - continuePipeline, result := target.SetOutputData(context, expected, "", "", "") - assert.True(t, continuePipeline) - assert.NotNil(t, result) - - actual := string(context.OutputData) - assert.Equal(t, expected, actual) -} - -func TestSetOutputDataBadType(t *testing.T) { - target := NewOutputData() +func TestSetResponseDataBadType(t *testing.T) { + target := NewResponseData() // Channels are not marshalable to JSON and generate an error - continuePipeline, result := target.SetOutputData(context, make(chan int)) + continuePipeline, result := target.SetResponseData(context, make(chan int)) assert.False(t, continuePipeline) require.NotNil(t, result) assert.Contains(t, result.(error).Error(), "passed in data must be of type") diff --git a/pkg/transforms/tags.go b/pkg/transforms/tags.go index 943bfbac8..321914c61 100644 --- a/pkg/transforms/tags.go +++ b/pkg/transforms/tags.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ import ( "errors" "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) // Tags contains the list of Tag key/values @@ -38,14 +38,14 @@ func NewTags(tags map[string]string) Tags { } // AddTags adds the pre-configured list of tags to the Event's tags collection. -func (t *Tags) AddTags(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("Adding tags to Event") +func (t *Tags) AddTags(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + ctx.LoggingClient().Debug("Adding tags to Event") - if len(params) < 1 { + if data == nil { return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } @@ -58,9 +58,9 @@ func (t *Tags) AddTags(edgexcontext *appcontext.Context, params ...interface{}) for tag, value := range t.tags { event.Tags[tag] = value } - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Tags added to Event. Event tags=%v", event.Tags)) + ctx.LoggingClient().Debug(fmt.Sprintf("Tags added to Event. Event tags=%v", event.Tags)) } else { - edgexcontext.LoggingClient.Debug("No tags added to Event. Add tags list is empty.") + ctx.LoggingClient().Debug("No tags added to Event. Add tags list is empty.") } return true, event diff --git a/pkg/transforms/tags_test.go b/pkg/transforms/tags_test.go index c57244908..d055e6f37 100644 --- a/pkg/transforms/tags_test.go +++ b/pkg/transforms/tags_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,9 +19,6 @@ package transforms import ( "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" "github.com/stretchr/testify/assert" @@ -50,10 +47,6 @@ var allTagsAdded = map[string]string{ } func TestTags_AddTags(t *testing.T) { - appContext := appcontext.Context{ - LoggingClient: logger.NewMockClient(), - } - tests := []struct { Name string FunctionInput interface{} @@ -77,9 +70,9 @@ func TestTags_AddTags(t *testing.T) { target := NewTags(testCase.TagsToAdd) if testCase.FunctionInput != nil { - continuePipeline, result = target.AddTags(&appContext, testCase.FunctionInput) + continuePipeline, result = target.AddTags(context, testCase.FunctionInput) } else { - continuePipeline, result = target.AddTags(&appContext) + continuePipeline, result = target.AddTags(context, nil) } if testCase.ErrorExpected { diff --git a/pkg/transforms/testmain_test.go b/pkg/transforms/testmain_test.go new file mode 100644 index 000000000..4eb1cbb78 --- /dev/null +++ b/pkg/transforms/testmain_test.go @@ -0,0 +1,60 @@ +// +// Copyright (c) 2021 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 transforms + +import ( + "os" + "testing" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" +) + +var lc logger.LoggingClient +var dic *di.Container +var context *appfunction.Context + +func TestMain(m *testing.M) { + lc = logger.NewMockClient() + eventClient := coredata.NewEventClient(local.New("http://test" + clients.ApiEventRoute)) + + config := &common.ConfigurationStruct{} + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return config + }, + container.EventClientName: func(get di.Get) interface{} { + return eventClient + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + }) + + context = appfunction.NewContext("123", dic, "") + + os.Exit(m.Run()) +} diff --git a/pkg/util/helpers.go b/pkg/util/helpers.go index 9e42a441c..688e0592e 100644 --- a/pkg/util/helpers.go +++ b/pkg/util/helpers.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ func DeleteEmptyAndTrim(s []string) []string { return r } -//CoerceType will accept a string, []byte, or json.Marshaler type and convert it to a []byte for use and consistency in the SDK +//CoerceType will accept a string, []byte, or json.Marshaller type and convert it to a []byte for use and consistency in the SDK func CoerceType(param interface{}) ([]byte, error) { var data []byte var err error diff --git a/pkg/util/helpers_test.go b/pkg/util/helpers_test.go index 7124282fd..476068626 100644 --- a/pkg/util/helpers_test.go +++ b/pkg/util/helpers_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,8 +44,8 @@ func TestSplitCommaEmpty(t *testing.T) { } func TestDeleteEmptyAndTrim(t *testing.T) { - strings := []string{" Hel lo", "test ", " "} - results := DeleteEmptyAndTrim(strings) + target := []string{" Hel lo", "test ", " "} + results := DeleteEmptyAndTrim(target) // Should have 4 elements (space counts as an element) assert.Equal(t, 2, len(results)) assert.Equal(t, "Hel lo", results[0]) From 8e77b665a4e93c6872ad1694f7b547815c2dd0d7 Mon Sep 17 00:00:00 2001 From: lenny Date: Mon, 8 Mar 2021 18:03:22 -0700 Subject: [PATCH 2/3] refactor: Rework SDK to use Interfaces and factory methods Added mocks for interfaces for use is custom service unit tests. Refactored to make better use of the dependency injection container (dic) to propagate the common dependencies through out the layers General file clean up for items flag by IDE as non-standard code. closes #573 BREAKING CHANGE: App Services will require refactoring to use new interfaces and factory methods Signed-off-by: lenny --- Makefile | 22 +- app-service-template/Attribution.txt | 5 +- app-service-template/Makefile | 16 + app-service-template/functions/sample.go | 49 +- app-service-template/functions/sample_test.go | 42 +- app-service-template/main.go | 38 +- app-service-template/main_test.go | 111 ++++ appcontext/context.go | 157 ----- appcontext/context_test.go | 105 ---- appsdk/sdk.go | 535 ------------------ appsdk/triggerfactory.go | 117 ---- go.mod | 1 - .../app}/backgroundpublisher.go | 16 +- .../app}/backgroundpublisher_test.go | 6 +- {appsdk => internal/app}/configupdates.go | 112 ++-- {appsdk => internal/app}/configurable.go | 154 ++--- {appsdk => internal/app}/configurable_test.go | 95 +--- internal/app/service.go | 532 +++++++++++++++++ .../app/service_test.go | 141 +++-- internal/app/triggerfactory.go | 121 ++++ .../app}/triggerfactory_test.go | 63 ++- internal/appfunction/context.go | 220 +++++++ internal/appfunction/context_test.go | 265 +++++++++ internal/bootstrap/container/config.go | 7 +- internal/bootstrap/handlers/clients.go | 29 +- internal/bootstrap/handlers/clients_test.go | 14 +- internal/bootstrap/handlers/storeclient.go | 10 +- internal/bootstrap/handlers/telemetry.go | 4 +- internal/bootstrap/handlers/version.go | 5 +- internal/bootstrap/handlers/version_test.go | 8 +- internal/common/clients.go | 38 -- internal/constants.go | 7 +- internal/controller/rest/controller.go | 18 +- internal/controller/rest/controller_test.go | 38 +- internal/runtime/runtime.go | 75 ++- internal/runtime/runtime_test.go | 191 +++---- internal/runtime/storeforward.go | 116 ++-- internal/runtime/storeforward_test.go | 83 +-- internal/store/db/db.go | 2 +- internal/store/db/redis/store.go | 12 +- internal/store/factory.go | 3 +- internal/telemetry/telemetry.go | 3 +- internal/telemetry/windows_cpu.go | 8 +- internal/trigger/http/rest.go | 84 +-- internal/trigger/http/rest_test.go | 11 +- internal/trigger/messagebus/messaging.go | 98 ++-- internal/trigger/messagebus/messaging_test.go | 214 ++++--- internal/trigger/mqtt/mqtt.go | 109 ++-- internal/webserver/server.go | 34 +- internal/webserver/server_test.go | 35 +- pkg/factory.go | 61 ++ pkg/factory_test.go | 56 ++ pkg/interfaces/backgroundpublisher.go | 25 + pkg/interfaces/context.go | 77 +++ pkg/interfaces/mocks/AppFunctionContext.go | 211 +++++++ pkg/interfaces/mocks/ApplicationService.go | 301 ++++++++++ pkg/interfaces/mocks/BackgroundPublisher.go | 15 + pkg/interfaces/mocks/Trigger.go | 43 ++ pkg/interfaces/mocks/TriggerContextBuilder.go | 31 + .../mocks/TriggerMessageProcessor.go | 29 + pkg/interfaces/service.go | 102 ++++ {appsdk => pkg/interfaces}/trigger.go | 31 +- pkg/secure/mqttfactory.go | 14 +- pkg/secure/mqttfactory_test.go | 49 +- pkg/transforms/batch.go | 26 +- pkg/transforms/batch_test.go | 18 +- pkg/transforms/compression.go | 52 +- pkg/transforms/compression_test.go | 8 +- pkg/transforms/conversion.go | 29 +- pkg/transforms/conversion_test.go | 78 +-- pkg/transforms/coredata.go | 20 +- pkg/transforms/coredata_test.go | 3 +- pkg/transforms/encryption.go | 22 +- pkg/transforms/encryption_test.go | 17 +- pkg/transforms/filter.go | 44 +- pkg/transforms/filter_test.go | 150 ++--- pkg/transforms/http.go | 40 +- pkg/transforms/http_test.go | 52 +- pkg/transforms/jsonlogic.go | 36 +- pkg/transforms/jsonlogic_test.go | 46 +- pkg/transforms/mqttsecret.go | 47 +- pkg/transforms/mqttsecret_broker_test.go | 44 -- pkg/transforms/mqttsecret_test.go | 14 +- .../{outputdata.go => responsedata.go} | 33 +- ...utputdata_test.go => responsedata_test.go} | 50 +- pkg/transforms/tags.go | 18 +- pkg/transforms/tags_test.go | 13 +- pkg/transforms/testmain_test.go | 60 ++ pkg/util/helpers.go | 4 +- pkg/util/helpers_test.go | 6 +- 90 files changed, 3761 insertions(+), 2393 deletions(-) create mode 100644 app-service-template/main_test.go delete mode 100644 appcontext/context.go delete mode 100644 appcontext/context_test.go delete mode 100644 appsdk/sdk.go delete mode 100644 appsdk/triggerfactory.go rename {appsdk => internal/app}/backgroundpublisher.go (74%) rename {appsdk => internal/app}/backgroundpublisher_test.go (96%) rename {appsdk => internal/app}/configupdates.go (55%) rename {appsdk => internal/app}/configurable.go (73%) rename {appsdk => internal/app}/configurable_test.go (88%) create mode 100644 internal/app/service.go rename appsdk/sdk_test.go => internal/app/service_test.go (80%) create mode 100644 internal/app/triggerfactory.go rename {appsdk => internal/app}/triggerfactory_test.go (81%) create mode 100644 internal/appfunction/context.go create mode 100644 internal/appfunction/context_test.go delete mode 100644 internal/common/clients.go create mode 100644 pkg/factory.go create mode 100644 pkg/factory_test.go create mode 100644 pkg/interfaces/backgroundpublisher.go create mode 100644 pkg/interfaces/context.go create mode 100644 pkg/interfaces/mocks/AppFunctionContext.go create mode 100644 pkg/interfaces/mocks/ApplicationService.go create mode 100644 pkg/interfaces/mocks/BackgroundPublisher.go create mode 100644 pkg/interfaces/mocks/Trigger.go create mode 100644 pkg/interfaces/mocks/TriggerContextBuilder.go create mode 100644 pkg/interfaces/mocks/TriggerMessageProcessor.go create mode 100644 pkg/interfaces/service.go rename {appsdk => pkg/interfaces}/trigger.go (64%) delete mode 100644 pkg/transforms/mqttsecret_broker_test.go rename pkg/transforms/{outputdata.go => responsedata.go} (54%) rename pkg/transforms/{outputdata_test.go => responsedata_test.go} (57%) create mode 100644 pkg/transforms/testmain_test.go diff --git a/Makefile b/Makefile index 7cbb04ef8..e6c18613d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2021 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. +# + .PHONY: test GO=CGO_ENABLED=1 GO111MODULE=on go @@ -8,9 +24,11 @@ build: test-template: make -C ./app-service-template test -test: build test-template +test-sdk: $(GO) test ./... -coverprofile=coverage.out ./... $(GO) vet ./... gofmt -l . [ "`gofmt -l .`" = "" ] - ./app-service-template/bin/test-go-mod-tidy.sh \ No newline at end of file + ./app-service-template/bin/test-go-mod-tidy.sh + +test: build test-template test-sdk \ No newline at end of file diff --git a/app-service-template/Attribution.txt b/app-service-template/Attribution.txt index 5c9181338..d0f067243 100644 --- a/app-service-template/Attribution.txt +++ b/app-service-template/Attribution.txt @@ -172,4 +172,7 @@ github.com/mattn/go-isatty (MIT) https://github.com/mattn/go-isatty https://github.com/mattn/go-isatty/blob/master/LICENSE golang.org/x/sys (Unspecified) https://github.com/golang/sys -https://github.com/golang/sys/blob/master/LICENSE \ No newline at end of file +https://github.com/golang/sys/blob/master/LICENSE + +stretchr/objx (MIT) https://github.com/stretchr/objx +https://github.com/stretchr/objx/blob/master/LICENSE \ No newline at end of file diff --git a/app-service-template/Makefile b/app-service-template/Makefile index 475b268b5..7e309a2b5 100644 --- a/app-service-template/Makefile +++ b/app-service-template/Makefile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2021 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. +# + .PHONY: build test clean docker GO=CGO_ENABLED=1 go diff --git a/app-service-template/functions/sample.go b/app-service-template/functions/sample.go index dbe7a3a7f..6e13fd82d 100644 --- a/app-service-template/functions/sample.go +++ b/app-service-template/functions/sample.go @@ -21,7 +21,10 @@ import ( "fmt" "strings" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" ) @@ -39,27 +42,28 @@ type Sample struct { // LogEventDetails is example of processing an Event and passing the original Event to to next function in the pipeline // For more details on the Context API got here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/ -func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("LogEventDetails called") +func (s *Sample) LogEventDetails(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("LogEventDetails called") - if len(params) < 1 { + if data == nil { // Go here for details on Error Handle: https://docs.edgexfoundry.org/1.3/microservices/application/ErrorHandling/ return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } - edgexcontext.LoggingClient.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d", + lc.Infof("Event received: ID=%s, Device=%s, and ReadingCount=%d", event.Id, event.DeviceName, len(event.Readings)) for index, reading := range event.Readings { switch strings.ToLower(reading.ValueType) { case strings.ToLower(v2.ValueTypeBinary): - edgexcontext.LoggingClient.Infof( + lc.Infof( "Reading #%d received with ID=%s, Resource=%s, ValueType=%s, MediaType=%s and BinaryValue of size=`%d`", index+1, reading.Id, @@ -68,7 +72,7 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int reading.MediaType, len(reading.BinaryValue)) default: - edgexcontext.LoggingClient.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`", + lc.Infof("Reading #%d received with ID=%s, Resource=%s, ValueType=%s, Value=`%s`", index+1, reading.Id, reading.ResourceName, @@ -83,14 +87,15 @@ func (s *Sample) LogEventDetails(edgexcontext *appcontext.Context, params ...int } // ConvertEventToXML is example of transforming an Event and passing the transformed data to to next function in the pipeline -func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("ConvertEventToXML called") +func (s *Sample) ConvertEventToXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("ConvertEventToXML called") - if len(params) < 1 { + if data == nil { return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } @@ -103,7 +108,7 @@ func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...i // Example of DEBUG message which by default you don't want to be logged. // To see debug log messages, Set WRITABLE_LOGLEVEL=DEBUG environment variable or // change LogLevel in configuration.toml before running app service. - edgexcontext.LoggingClient.Debug("Event converted to XML: " + xml) + lc.Debug("Event converted to XML: " + xml) // Returning true indicates that the pipeline execution should continue with the next function // using the event passed as input in this case. @@ -111,26 +116,28 @@ func (s *Sample) ConvertEventToXML(edgexcontext *appcontext.Context, params ...i } // OutputXML is an example of processing transformed data -func (s *Sample) OutputXML(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("OutputXML called") +func (s *Sample) OutputXML(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + lc := ctx.LoggingClient() + lc.Debug("OutputXML called") - if len(params) < 1 { + if data == nil { return false, errors.New("no XML Received") } - xml, ok := params[0].(string) + xml, ok := data.(string) if !ok { return false, errors.New("type received is not an string") } - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Outputting the following XML: %s", xml)) + lc.Debug(fmt.Sprintf("Outputting the following XML: %s", xml)) // This sends the XML as a response. i.e. publish for MessageBus/MQTT triggers as configured or // HTTP response to for the HTTP Trigger - // For more details on the Complete() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete - edgexcontext.Complete([]byte(xml)) + // For more details on the SetResponseData() function go here: https://docs.edgexfoundry.org/1.3/microservices/application/ContextAPI/#complete + ctx.SetResponseData([]byte(xml)) + ctx.SetResponseContentType(clients.ContentTypeXML) // Returning false terminates the pipeline execution, so this should be last function specified in the pipeline, - // which is typical in conjunction with usage of .Complete() function. + // which is typical in conjunction with usage of .SetResponseData() function. return false, nil } diff --git a/app-service-template/functions/sample_test.go b/app-service-template/functions/sample_test.go index 3c91250fd..11b309062 100644 --- a/app-service-template/functions/sample_test.go +++ b/app-service-template/functions/sample_test.go @@ -19,12 +19,15 @@ package functions import ( "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" "github.com/stretchr/testify/require" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -32,12 +35,28 @@ import ( // This file contains example of how to unit test pipeline functions // TODO: Change these sample unit tests to test your custom type and function(s) +var appContext interfaces.AppFunctionContext + +func TestMain(m *testing.M) { + // + // This can be changed to a real logger when needing more debug information output to the console + // lc := logger.NewClient("testing", "DEBUG") + // + lc := logger.NewMockClient() + correlationId := uuid.New().String() + + // NewAppFuncContextForTest creates a context with basic dependencies for unit testing with the passed in logger + // If more additional dependencies (such as mock clients) are required, then use + // NewAppFuncContext(correlationID string, dic *di.Container) and pass in an initialized DIC (dependency injection container) + appContext = pkg.NewAppFuncContextForTest(correlationId, lc) +} + func TestSample_LogEventDetails(t *testing.T) { expectedEvent := createTestEvent(t) expectedContinuePipeline := true target := NewSample() - actualContinuePipeline, actualEvent := target.LogEventDetails(createTestAppSdkContext(), expectedEvent) + actualContinuePipeline, actualEvent := target.LogEventDetails(appContext, expectedEvent) assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Equal(t, expectedEvent, actualEvent) @@ -49,7 +68,7 @@ func TestSample_ConvertEventToXML(t *testing.T) { expectedContinuePipeline := true target := NewSample() - actualContinuePipeline, actualXml := target.ConvertEventToXML(createTestAppSdkContext(), event) + actualContinuePipeline, actualXml := target.ConvertEventToXML(appContext, event) assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Equal(t, expectedXml, actualXml) @@ -58,17 +77,17 @@ func TestSample_ConvertEventToXML(t *testing.T) { func TestSample_OutputXML(t *testing.T) { testEvent := createTestEvent(t) - expectedXml, _ := testEvent.ToXML() + xml, _ := testEvent.ToXML() expectedContinuePipeline := false - appContext := createTestAppSdkContext() + expectedContentType := clients.ContentTypeXML target := NewSample() - actualContinuePipeline, result := target.OutputXML(appContext, expectedXml) - actualXml := string(appContext.OutputData) + actualContinuePipeline, result := target.OutputXML(appContext, xml) + actualContentType := appContext.ResponseContentType() assert.Equal(t, expectedContinuePipeline, actualContinuePipeline) assert.Nil(t, result) - assert.Equal(t, expectedXml, actualXml) + assert.Equal(t, expectedContentType, actualContentType) } func createTestEvent(t *testing.T) dtos.Event { @@ -87,10 +106,3 @@ func createTestEvent(t *testing.T) dtos.Event { return event } - -func createTestAppSdkContext() *appcontext.Context { - return &appcontext.Context{ - CorrelationID: uuid.New().String(), - LoggingClient: logger.NewMockClient(), - } -} diff --git a/app-service-template/main.go b/app-service-template/main.go index 10e7d2278..e916402f8 100644 --- a/app-service-template/main.go +++ b/app-service-template/main.go @@ -20,7 +20,8 @@ package main import ( "os" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appsdk" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "new-app-service/functions" @@ -34,38 +35,45 @@ func main() { // TODO: See https://docs.edgexfoundry.org/1.3/microservices/application/ApplicationServices/ // for documentation on application services. - edgexSdk := &appsdk.AppFunctionsSDK{ServiceKey: serviceKey} - if err := edgexSdk.Initialize(); err != nil { - edgexSdk.LoggingClient.Errorf("SDK initialization failed: %s", err.Error()) - os.Exit(-1) + code := CreateAndRunService(serviceKey, pkg.NewAppService) + os.Exit(code) +} + +// CreateAndRunService wraps what would normally be in main() so that it can be unit tested +func CreateAndRunService(serviceKey string, newServiceFactory func(string) (interfaces.ApplicationService, bool)) int { + service, ok := newServiceFactory(serviceKey) + if !ok { + return -1 } + lc := service.LoggingClient() + // TODO: Replace with retrieving your custom ApplicationSettings from configuration - deviceNames, err := edgexSdk.GetAppSettingStrings("DeviceNames") + deviceNames, err := service.GetAppSettingStrings("DeviceNames") if err != nil { - edgexSdk.LoggingClient.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error()) - os.Exit(-1) + lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error()) + return -1 } // TODO: Replace below functions with built in and/or your custom functions for your use case. // See https://docs.edgexfoundry.org/1.3/microservices/application/BuiltIn/ for list of built-in functions sample := functions.NewSample() - err = edgexSdk.SetFunctionsPipeline( + err = service.SetFunctionsPipeline( transforms.NewFilterFor(deviceNames).FilterByDeviceName, sample.LogEventDetails, sample.ConvertEventToXML, sample.OutputXML) if err != nil { - edgexSdk.LoggingClient.Errorf("SetFunctionsPipeline returned error: %s", err.Error()) - os.Exit(-1) + lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error()) + return -1 } - if err := edgexSdk.MakeItRun(); err != nil { - edgexSdk.LoggingClient.Errorf("MakeItRun returned error: %s", err.Error()) - os.Exit(-1) + if err := service.MakeItRun(); err != nil { + lc.Errorf("MakeItRun returned error: %s", err.Error()) + return -1 } // TODO: Do any required cleanup here, if needed - os.Exit(0) + return 0 } diff --git a/app-service-template/main_test.go b/app-service-template/main_test.go new file mode 100644 index 000000000..1e8c7fada --- /dev/null +++ b/app-service-template/main_test.go @@ -0,0 +1,111 @@ +// TODO: Change Copyright to your company if open sourcing or remove header +// +// Copyright (c) 2021 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 main + +import ( + "fmt" + "testing" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces/mocks" +) + +// This is an example of how to test the code that would typically be in the main() function use mocks +// Not to helpful for a simple main() , but can be if the main() has more complexity that should be unit tested +// TODO: add/update tests for your customized CreateAndRunService or remove for simple main() + +func TestCreateAndRunService_Success(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + mockAppService.On("MakeItRun").Return(nil) + + return mockAppService, true + } + + expected := 0 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_NewService_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + return nil, false + } + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_GetAppSettingStrings_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return(nil, fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_SetFunctionsPipeline_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} + +func TestCreateAndRunService_MakeItRun_Failed(t *testing.T) { + mockFactory := func(_ string) (interfaces.ApplicationService, bool) { + mockAppService := &mocks.ApplicationService{} + mockAppService.On("LoggingClient").Return(logger.NewMockClient()) + mockAppService.On("GetAppSettingStrings", "DeviceNames"). + Return([]string{"Random-Boolean-Device, Random-Integer-Device"}, nil) + mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil) + mockAppService.On("MakeItRun").Return(fmt.Errorf("Failed")) + + return mockAppService, true + } + + expected := -1 + actual := CreateAndRunService("TestKey", mockFactory) + assert.Equal(t, expected, actual) +} diff --git a/appcontext/context.go b/appcontext/context.go deleted file mode 100644 index 627339def..000000000 --- a/appcontext/context.go +++ /dev/null @@ -1,157 +0,0 @@ -// -// 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 appcontext - -import ( - "context" - "fmt" - "time" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" - "github.com/edgexfoundry/go-mod-core-contracts/v2/models" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" - - "github.com/google/uuid" -) - -// AppFunction is a type alias for func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) -type AppFunction = func(edgexcontext *Context, params ...interface{}) (bool, interface{}) - -// Context ... -type Context struct { - // This is the ID used to track the EdgeX event through entire EdgeX framework. - CorrelationID string - // 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 - // LoggingClient is exposed to allow logging following the preferred logging strategy within EdgeX. - LoggingClient logger.LoggingClient - // EventClient exposes Core Data's EventClient API - EventClient coredata.EventClient - // ValueDescriptorClient exposes Core Data's ValueDescriptor API - ValueDescriptorClient coredata.ValueDescriptorClient - // CommandClient exposes Core Commands' Command API - CommandClient command.CommandClient - // NotificationsClient exposes Support Notification's Notifications API - NotificationsClient notifications.NotificationsClient - // RetryData holds the data to be stored for later retry when the pipeline function returns an error - RetryData []byte - // SecretProvider exposes the support for getting and storing secrets - SecretProvider interfaces.SecretProvider - // ResponseContentType is used for holding custom response type for HTTP trigger - ResponseContentType string -} - -// Complete is optional and provides a way to return the specified data. -// In the case of an HTTP Trigger, the data will be returned as the http response. -// In the case of the message bus trigger, the data will be placed on the specified -// message bus publish topic and host in the configuration. -func (appContext *Context) Complete(output []byte) { - appContext.OutputData = output -} - -// SetRetryData sets the RetryData to the specified payload to be stored for later retry -// when the pipeline function returns an error. -func (appContext *Context) SetRetryData(payload []byte) { - appContext.RetryData = payload -} - -// PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in -// CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. -func (appContext *Context) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { - appContext.LoggingClient.Debug("Pushing to CoreData") - if appContext.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 { - return nil, err - } - - // Temporary use V1 Reading until V2 EventClient is available - // TODO: Change to use dtos.Reading - v1Reading := models.Reading{ - Value: string(val), - ValueType: v2.ValueTypeString, - Origin: now, - Device: deviceName, - Name: readingName, - } - - readings := make([]models.Reading, 0, 1) - readings = append(readings, v1Reading) - - // Temporary use V1 Event until V2 EventClient is available - // TODO: Change to use dtos.Event - v1Event := &models.Event{ - Device: deviceName, - Origin: now, - Readings: readings, - } - - correlation := uuid.New().String() - ctx := context.WithValue(context.Background(), clients.CorrelationHeader, correlation) - result, err := appContext.EventClient.Add(ctx, v1Event) // TODO: Update to use V2 EventClient - if err != nil { - return nil, err - } - v1Event.ID = result - - // TODO: Remove once V2 EventClient is available - v2Reading := dtos.BaseReading{ - Versionable: commonDTO.NewVersionable(), - Id: v1Reading.Id, - Created: v1Reading.Created, - Origin: v1Reading.Origin, - DeviceName: v1Reading.Device, - ResourceName: v1Reading.Name, - ProfileName: "", - ValueType: v1Reading.ValueType, - SimpleReading: dtos.SimpleReading{Value: v1Reading.Value}, - } - - // TODO: Remove once V2 EventClient is available - v2Event := dtos.Event{ - Versionable: commonDTO.NewVersionable(), - Id: result, - DeviceName: v1Event.Device, - Origin: v1Event.Origin, - Readings: []dtos.BaseReading{v2Reading}, - } - return &v2Event, nil -} - -// GetSecrets retrieves secrets from a secret store. -// path specifies the type or location of the secrets to retrieve. -// 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 (appContext *Context) GetSecrets(path string, keys ...string) (map[string]string, error) { - return appContext.SecretProvider.GetSecrets(path, keys...) -} diff --git a/appcontext/context_test.go b/appcontext/context_test.go deleted file mode 100644 index 0ed00e65a..000000000 --- a/appcontext/context_test.go +++ /dev/null @@ -1,105 +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 appcontext - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - localURL "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestComplete(t *testing.T) { - ctx := Context{} - testData := "output data" - ctx.Complete([]byte(testData)) - assert.Equal(t, []byte(testData), ctx.OutputData) -} - -var eventClient coredata.EventClient -var lc logger.LoggingClient - -func init() { - eventClient = coredata.NewEventClient(localURL.New("http://test" + clients.ApiEventRoute)) - lc = logger.NewMockClient() -} - -func TestPushToCore(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("newId")) - if r.Method != http.MethodPost { - t.Errorf("expected http method is POST, active http method is : %s", r.Method) - } - url := clients.ApiEventRoute - if r.URL.EscapedPath() != url { - t.Errorf("expected uri path is %s, actual uri path is %s", url, r.URL.EscapedPath()) - } - - })) - - defer ts.Close() - eventClient = coredata.NewEventClient(localURL.New(ts.URL + clients.ApiEventRoute)) - ctx := Context{ - EventClient: eventClient, - LoggingClient: lc, - } - expectedEvent := &dtos.Event{ - Versionable: common.NewVersionable(), - DeviceName: "device-name", - Readings: []dtos.BaseReading{ - { - Versionable: common.NewVersionable(), - DeviceName: "device-name", - ResourceName: "device-resource", - ValueType: v2.ValueTypeString, - SimpleReading: dtos.SimpleReading{ - Value: "value", - }, - }, - }, - } - actualEvent, err := ctx.PushToCoreData("device-name", "device-resource", "value") - require.NoError(t, err) - - assert.NotNil(t, actualEvent) - assert.Equal(t, expectedEvent.ApiVersion, actualEvent.ApiVersion) - assert.Equal(t, expectedEvent.DeviceName, actualEvent.DeviceName) - assert.True(t, len(expectedEvent.Readings) == 1) - assert.Equal(t, expectedEvent.Readings[0].DeviceName, actualEvent.Readings[0].DeviceName) - assert.Equal(t, expectedEvent.Readings[0].ResourceName, actualEvent.Readings[0].ResourceName) - assert.Equal(t, expectedEvent.Readings[0].Value, actualEvent.Readings[0].Value) - assert.Equal(t, expectedEvent.Readings[0].ValueType, actualEvent.Readings[0].ValueType) - assert.Equal(t, expectedEvent.Readings[0].ApiVersion, actualEvent.Readings[0].ApiVersion) -} - -func TestSetRetryData(t *testing.T) { - ctx := Context{} - testData := "output data" - ctx.SetRetryData([]byte(testData)) - assert.Equal(t, []byte(testData), ctx.RetryData) -} diff --git a/appsdk/sdk.go b/appsdk/sdk.go deleted file mode 100644 index 7db7e4172..000000000 --- a/appsdk/sdk.go +++ /dev/null @@ -1,535 +0,0 @@ -// -// 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" - "errors" - "fmt" - nethttp "net/http" - "os" - "os/signal" - "reflect" - "strings" - "sync" - "syscall" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" - bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" - bootstrapHandlers "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/handlers" - bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" - "github.com/edgexfoundry/go-mod-bootstrap/v2/di" - "github.com/edgexfoundry/go-mod-messaging/v2/messaging" - "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/edgexfoundry/go-mod-registry/v2/registry" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/models" - - "github.com/gorilla/mux" -) - -const ( - // ProfileSuffixPlaceholder is used to create unique names for profiles - ProfileSuffixPlaceholder = "" - envProfile = "EDGEX_PROFILE" - envServiceKey = "EDGEX_SERVICE_KEY" - - TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" - TriggerTypeMQTT = "EXTERNAL-MQTT" - TriggerTypeHTTP = "HTTP" - - OptionalPasswordKey = "Password" -) - -// The key type is unexported to prevent collisions with context keys defined in -// other packages. -type key int - -// SDKKey is the context key for getting the sdk context. Its value of zero is -// arbitrary. If this package defined other context keys, they would have -// different integer values. -const SDKKey key = 0 - -// AppFunctionsSDK provides the necessary struct to create an instance of the Application Functions SDK. Be sure and provide a ServiceKey -// when creating an instance of the SDK. After creating an instance, you'll first want to call .Initialize(), to start up the SDK. Secondly, -// provide the desired transforms for your pipeline by calling .SetFunctionsPipeline(). Lastly, call .MakeItRun() to start listening for events based on -// your configured trigger. -type AppFunctionsSDK struct { - // ServiceKey is the application services' key used for Configuration and Registration when the Registry is enabled - ServiceKey string - // LoggingClient is the EdgeX logger client used to log messages - LoggingClient logger.LoggingClient - // TargetType is the expected type of the incoming data. Must be set to a pointer to an instance of the type. - // Defaults to &models.Event{} if nil. The income data is un-marshaled (JSON or CBOR) in to the type, - // except when &[]byte{} is specified. In this case the []byte data is pass to the first function in the Pipeline. - TargetType interface{} - // EdgexClients allows access to the EdgeX clients such as the CommandClient. - // Note that the individual clients (e.g EdgexClients.CommandClient) may be nil if the Service (Command) is not configured - // in the [Clients] section of the App Service's configuration. - // It is highly recommend that the clients are verified to not be nil before use. - EdgexClients common.EdgeXClients - // RegistryClient is the client used by service to communicate with service registry. - RegistryClient registry.Client - transforms []appcontext.AppFunction - skipVersionCheck bool - usingConfigurablePipeline bool - httpErrors chan error - runtime *runtime.GolangRuntime - webserver *webserver.WebServer - config *common.ConfigurationStruct - storeClient interfaces.StoreClient - secretProvider bootstrapInterfaces.SecretProvider - storeForwardWg *sync.WaitGroup - storeForwardCancelCtx context.CancelFunc - appWg *sync.WaitGroup - appCtx context.Context - appCancelCtx context.CancelFunc - deferredFunctions []bootstrap.Deferred - serviceKeyOverride string - backgroundChannel <-chan types.MessageEnvelope - customTriggerFactories map[string]func(sdk *AppFunctionsSDK) (Trigger, error) - stop context.CancelFunc -} - -// AddRoute allows you to leverage the existing webserver to add routes. -func (sdk *AppFunctionsSDK) AddRoute(route string, handler func(nethttp.ResponseWriter, *nethttp.Request), methods ...string) error { - if route == clients.ApiPingRoute || - route == clients.ApiConfigRoute || - route == clients.ApiMetricsRoute || - route == clients.ApiVersionRoute || - route == internal.ApiTriggerRoute { - return errors.New("route is reserved") - } - return sdk.webserver.AddRoute(route, sdk.addContext(handler), methods...) -} - -// AddBackgroundPublisher will create a channel of provided capacity to be -// consumed by the MessageBus output and return a publisher that writes to it -func (sdk *AppFunctionsSDK) AddBackgroundPublisher(capacity int) BackgroundPublisher { - bgchan, pub := newBackgroundPublisher(capacity) - sdk.backgroundChannel = bgchan - return pub -} - -// MakeItStop will force the service loop to exit in the same fashion as SIGINT/SIGTERM received from the OS -func (sdk *AppFunctionsSDK) MakeItStop() { - if sdk.stop != nil { - sdk.stop() - } else { - sdk.LoggingClient.Warn("MakeItStop called but no stop handler set on SDK - is the service running?") - } -} - -// MakeItRun will initialize and start the trigger as specified in the -// configuration. It will also configure the webserver and start listening on -// the specified port. -func (sdk *AppFunctionsSDK) MakeItRun() error { - runCtx, stop := context.WithCancel(context.Background()) - - sdk.stop = stop - - httpErrors := make(chan error) - defer close(httpErrors) - - sdk.runtime = &runtime.GolangRuntime{ - TargetType: sdk.TargetType, - ServiceKey: sdk.ServiceKey, - } - - sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) - sdk.runtime.SetTransforms(sdk.transforms) - - // determine input type and create trigger for it - t := sdk.setupTrigger(sdk.config, sdk.runtime) - if t == nil { - return errors.New("Failed to create Trigger") - } - - // Initialize the trigger (i.e. start a web server, or connect to message bus) - deferred, err := t.Initialize(sdk.appWg, sdk.appCtx, sdk.backgroundChannel) - if err != nil { - sdk.LoggingClient.Error(err.Error()) - return errors.New("Failed to initialize Trigger") - } - - // 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 { - sdk.LoggingClient.Info("StoreAndForward disabled. Not running retry loop.") - } - - sdk.LoggingClient.Info(sdk.config.Service.StartupMsg) - - signals := make(chan os.Signal) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - - sdk.webserver.StartWebServer(sdk.httpErrors) - - select { - case httpError := <-sdk.httpErrors: - sdk.LoggingClient.Info("Http error received: ", httpError.Error()) - err = httpError - - case signalReceived := <-signals: - sdk.LoggingClient.Info("Terminating signal received: " + signalReceived.String()) - - case <-runCtx.Done(): - sdk.LoggingClient.Info("Terminating: sdk.MakeItStop called") - } - - sdk.stop = nil - - if sdk.config.Writable.StoreAndForward.Enabled { - sdk.storeForwardCancelCtx() - sdk.storeForwardWg.Wait() - } - - 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 -} - -// LoadConfigurablePipeline ... -func (sdk *AppFunctionsSDK) LoadConfigurablePipeline() ([]appcontext.AppFunction, error) { - var pipeline []appcontext.AppFunction - - sdk.usingConfigurablePipeline = true - - sdk.TargetType = nil - - if sdk.config.Writable.Pipeline.UseTargetTypeOfByteArray { - sdk.TargetType = &[]byte{} - } - - configurable := AppFunctionsSDKConfigurable{ - Sdk: sdk, - } - valueOfType := reflect.ValueOf(configurable) - pipelineConfig := sdk.config.Writable.Pipeline - executionOrder := util.DeleteEmptyAndTrim(strings.FieldsFunc(pipelineConfig.ExecutionOrder, util.SplitComma)) - - if len(executionOrder) <= 0 { - return nil, errors.New( - "execution Order has 0 functions specified. You must have a least one function in the pipeline") - } - sdk.LoggingClient.Debugf("Function Pipeline Execution Order: [%s]", pipelineConfig.ExecutionOrder) - - for _, functionName := range executionOrder { - functionName = strings.TrimSpace(functionName) - configuration, ok := pipelineConfig.Functions[functionName] - if !ok { - return nil, fmt.Errorf("function '%s' configuration not found in Pipeline.Functions section", functionName) - } - - result := valueOfType.MethodByName(functionName) - if result.Kind() == reflect.Invalid { - return nil, fmt.Errorf("function %s is not a built in SDK function", functionName) - } else if result.IsNil() { - return nil, fmt.Errorf("invalid/missing configuration for %s", functionName) - } - - // determine number of parameters required for function call - inputParameters := make([]reflect.Value, result.Type().NumIn()) - // set keys to be all lowercase to avoid casing issues from configuration - for key := range configuration.Parameters { - value := configuration.Parameters[key] - delete(configuration.Parameters, key) // Make sure the old key has been removed so don't have multiples - configuration.Parameters[strings.ToLower(key)] = value - } - for index := range inputParameters { - parameter := result.Type().In(index) - - switch parameter { - case reflect.TypeOf(map[string]string{}): - inputParameters[index] = reflect.ValueOf(configuration.Parameters) - - default: - return nil, fmt.Errorf( - "function %s has an unsupported parameter type: %s", - functionName, - parameter.String(), - ) - } - } - - function, ok := result.Call(inputParameters)[0].Interface().(appcontext.AppFunction) - if !ok { - return nil, fmt.Errorf("failed to cast function %s as AppFunction type", functionName) - } - - if function == nil { - return nil, fmt.Errorf("%s from configuration failed", functionName) - } - - pipeline = append(pipeline, function) - configurable.Sdk.LoggingClient.Debugf( - "%s function added to configurable pipeline with parameters: [%s]", - functionName, - listParameters(configuration.Parameters)) - } - - return pipeline, nil -} - -func listParameters(parameters map[string]string) string { - result := "" - first := true - for key, value := range parameters { - if first { - result = fmt.Sprintf("%s='%s'", key, value) - first = false - continue - } - - result += fmt.Sprintf(", %s='%s'", key, value) - } - - return result -} - -// SetFunctionsPipeline allows you to define each function to execute and the order in which each function -// will be called as each event comes in. -func (sdk *AppFunctionsSDK) SetFunctionsPipeline(transforms ...appcontext.AppFunction) error { - if len(transforms) == 0 { - return errors.New("no transforms provided to pipeline") - } - - sdk.transforms = transforms - - if sdk.runtime != nil { - sdk.runtime.SetTransforms(transforms) - sdk.runtime.TargetType = sdk.TargetType - } - - return nil -} - -// ApplicationSettings returns the values specified in the custom configuration section. -func (sdk *AppFunctionsSDK) ApplicationSettings() map[string]string { - return sdk.config.ApplicationSettings -} - -// GetAppSettingStrings returns the strings slice for the specified App Setting. -func (sdk *AppFunctionsSDK) GetAppSettingStrings(setting string) ([]string, error) { - if sdk.config.ApplicationSettings == nil { - return nil, fmt.Errorf("%s setting not found: ApplicationSettings section is missing", setting) - } - - settingValue, ok := sdk.config.ApplicationSettings[setting] - if !ok { - return nil, fmt.Errorf("%s setting not found in ApplicationSettings", setting) - } - - valueStrings := util.DeleteEmptyAndTrim(strings.FieldsFunc(settingValue, util.SplitComma)) - - return valueStrings, nil -} - -// Initialize will parse command line flags, register for interrupts, -// initialize the logging system, and ingest configuration. -func (sdk *AppFunctionsSDK) Initialize() error { - startupTimer := startup.NewStartUpTimer(sdk.ServiceKey) - - additionalUsage := - " -s/--skipVersionCheck Indicates the service should skip the Core Service's version compatibility check.\n" + - " -sk/--serviceKey Overrides the service service key used with Registry and/or Configuration Providers.\n" + - " If the name provided contains the text ``, this text will be replaced with\n" + - " the name of the profile used." - - sdkFlags := flags.NewWithUsage(additionalUsage) - sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "skipVersionCheck", false, "") - sdkFlags.FlagSet.BoolVar(&sdk.skipVersionCheck, "s", false, "") - sdkFlags.FlagSet.StringVar(&sdk.serviceKeyOverride, "serviceKey", "", "") - sdkFlags.FlagSet.StringVar(&sdk.serviceKeyOverride, "sk", "", "") - - sdkFlags.Parse(os.Args[1:]) - - // Temporarily setup logging to STDOUT so the client can be used before bootstrapping is completed - sdk.LoggingClient = logger.NewClient(sdk.ServiceKey, models.InfoLog) - - sdk.setServiceKey(sdkFlags.Profile()) - - sdk.LoggingClient.Info(fmt.Sprintf("Starting %s %s ", sdk.ServiceKey, internal.ApplicationVersion)) - - 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{} - - 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{ - bootstrapHandlers.SecureProviderBootstrapHandler, - 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 = bootstrapContainer.SecretProviderFrom(dic.Get) - sdk.storeClient = container.StoreClientFrom(dic.Get) - sdk.LoggingClient = bootstrapContainer.LoggingClientFrom(dic.Get) - sdk.RegistryClient = bootstrapContainer.RegistryFrom(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) - - // If using the RedisStreams MessageBus implementation then need to make sure the - // password for the Redis DB is set in the MessageBus Optional properties. - triggerType := strings.ToUpper(sdk.config.Trigger.Type) - if triggerType == TriggerTypeMessageBus && - sdk.config.Trigger.EdgexMessageBus.Type == messaging.RedisStreams { - - credentials, err := sdk.secretProvider.GetSecrets(sdk.config.Database.Type) - if err != nil { - return fmt.Errorf("unable to set RedisStreams password from DB credentials") - } - sdk.config.Trigger.EdgexMessageBus.Optional[OptionalPasswordKey] = credentials[secret.PasswordKey] - } - - // 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.ConfigureStandardRoutes() - - sdk.LoggingClient.Info("Service started in: " + startupTimer.SinceAsString()) - - return nil -} - -// 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) addContext(next func(nethttp.ResponseWriter, *nethttp.Request)) func(nethttp.ResponseWriter, *nethttp.Request) { - return func(w nethttp.ResponseWriter, r *nethttp.Request) { - ctx := context.WithValue(r.Context(), SDKKey, sdk) - next(w, r.WithContext(ctx)) - } -} - -func (sdk *AppFunctionsSDK) addDeferred(deferred bootstrap.Deferred) { - if deferred != nil { - sdk.deferredFunctions = append(sdk.deferredFunctions, deferred) - } -} - -// setServiceKey creates the service's service key with profile name if the original service key has the -// appropriate profile placeholder, otherwise it leaves the original service key unchanged -func (sdk *AppFunctionsSDK) setServiceKey(profile string) { - envValue := os.Getenv(envServiceKey) - if len(envValue) > 0 { - sdk.serviceKeyOverride = envValue - sdk.LoggingClient.Info( - fmt.Sprintf("Environment profileOverride of '-n/--serviceName' by environment variable: %s=%s", - envServiceKey, - envValue)) - } - - // serviceKeyOverride may have been set by the -n/--serviceName command-line option and not the environment variable - if len(sdk.serviceKeyOverride) > 0 { - sdk.ServiceKey = sdk.serviceKeyOverride - } - - if !strings.Contains(sdk.ServiceKey, ProfileSuffixPlaceholder) { - // No placeholder, so nothing to do here - return - } - - // Have to handle environment override here before common bootstrap is used so it is passed the proper service key - profileOverride := os.Getenv(envProfile) - if len(profileOverride) > 0 { - profile = profileOverride - } - - if len(profile) > 0 { - sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, profile, 1) - return - } - - // No profile specified so remove the placeholder text - sdk.ServiceKey = strings.Replace(sdk.ServiceKey, ProfileSuffixPlaceholder, "", 1) -} diff --git a/appsdk/triggerfactory.go b/appsdk/triggerfactory.go deleted file mode 100644 index 56b0d83f5..000000000 --- a/appsdk/triggerfactory.go +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright (c) 2020 Technocrats -// -// 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 ( - "errors" - "fmt" - "strings" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" - - "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" -) - -func (sdk *AppFunctionsSDK) defaultTriggerMessageProcessor(edgexcontext *appcontext.Context, envelope types.MessageEnvelope) error { - messageError := sdk.runtime.ProcessMessage(edgexcontext, envelope) - - if messageError != nil { - // ProcessMessage logs the error, so no need to log it here. - return messageError.Err - } - - return nil -} - -func (sdk *AppFunctionsSDK) defaultTriggerContextBuilder(env types.MessageEnvelope) *appcontext.Context { - return &appcontext.Context{ - CorrelationID: env.CorrelationID, - Configuration: sdk.config, - LoggingClient: sdk.LoggingClient, - EventClient: sdk.EdgexClients.EventClient, - ValueDescriptorClient: sdk.EdgexClients.ValueDescriptorClient, - CommandClient: sdk.EdgexClients.CommandClient, - NotificationsClient: sdk.EdgexClients.NotificationsClient, - SecretProvider: sdk.secretProvider, - } -} - -// RegisterCustomTriggerFactory allows users to register builders for custom trigger types -func (sdk *AppFunctionsSDK) RegisterCustomTriggerFactory(name string, - factory func(TriggerConfig) (Trigger, error)) error { - nu := strings.ToUpper(name) - - if nu == TriggerTypeMessageBus || - nu == TriggerTypeHTTP || - nu == TriggerTypeMQTT { - return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) - } - - if sdk.customTriggerFactories == nil { - sdk.customTriggerFactories = make(map[string]func(sdk *AppFunctionsSDK) (Trigger, error), 1) - } - - sdk.customTriggerFactories[nu] = func(sdk *AppFunctionsSDK) (Trigger, error) { - return factory(TriggerConfig{ - Config: sdk.config, - Logger: sdk.LoggingClient, - ContextBuilder: sdk.defaultTriggerContextBuilder, - MessageProcessor: sdk.defaultTriggerMessageProcessor, - }) - } - - return nil -} - -// setupTrigger configures the appropriate trigger as specified by configuration. -func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) Trigger { - var t Trigger - // Need to make dynamic, search for the trigger that is input - - switch triggerType := strings.ToUpper(configuration.Trigger.Type); triggerType { - case TriggerTypeHTTP: - sdk.LoggingClient.Info("HTTP trigger selected") - t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients} - - case TriggerTypeMessageBus: - sdk.LoggingClient.Info("EdgeX MessageBus trigger selected") - t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients} - - case TriggerTypeMQTT: - sdk.LoggingClient.Info("External MQTT trigger selected") - t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider) - - default: - if factory, found := sdk.customTriggerFactories[triggerType]; found { - var err error - t, err = factory(sdk) - if err != nil { - sdk.LoggingClient.Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) - return nil - } - } else { - sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type)) - } - } - - return t -} diff --git a/go.mod b/go.mod index ec9542a01..577e7c314 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,5 @@ require ( github.com/gomodule/redigo v2.0.0+incompatible github.com/google/uuid v1.2.0 github.com/gorilla/mux v1.8.0 - github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.7.0 ) diff --git a/appsdk/backgroundpublisher.go b/internal/app/backgroundpublisher.go similarity index 74% rename from appsdk/backgroundpublisher.go rename to internal/app/backgroundpublisher.go index f2d9728f4..b49f2fa0a 100644 --- a/appsdk/backgroundpublisher.go +++ b/internal/app/backgroundpublisher.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +15,13 @@ // limitations under the License. // -package appsdk +package app -import "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +import ( + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" -// BackgroundPublisher provides an interface to send messages from background processes -// through the service's configured MessageBus output -type BackgroundPublisher interface { - // Publish provided message through the configured MessageBus output - Publish(payload []byte, correlationID string, contentType string) -} + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) type backgroundPublisher struct { output chan<- types.MessageEnvelope @@ -40,7 +38,7 @@ func (pub *backgroundPublisher) Publish(payload []byte, correlationID string, co pub.output <- outputEnvelope } -func newBackgroundPublisher(capacity int) (<-chan types.MessageEnvelope, BackgroundPublisher) { +func newBackgroundPublisher(capacity int) (<-chan types.MessageEnvelope, interfaces.BackgroundPublisher) { backgroundChannel := make(chan types.MessageEnvelope, capacity) return backgroundChannel, &backgroundPublisher{output: backgroundChannel} } diff --git a/appsdk/backgroundpublisher_test.go b/internal/app/backgroundpublisher_test.go similarity index 96% rename from appsdk/backgroundpublisher_test.go rename to internal/app/backgroundpublisher_test.go index 9cc55ce71..5053581f4 100644 --- a/appsdk/backgroundpublisher_test.go +++ b/internal/app/backgroundpublisher_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,12 +15,13 @@ // limitations under the License. // -package appsdk +package app import ( - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestNewBackgroundPublisherAndPublish(t *testing.T) { diff --git a/appsdk/configupdates.go b/internal/app/configupdates.go similarity index 55% rename from appsdk/configupdates.go rename to internal/app/configupdates.go index f47a4050f..e046650d5 100644 --- a/appsdk/configupdates.go +++ b/internal/app/configupdates.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,86 +13,78 @@ // See the License for the specific language governing permissions and // limitations under the License. -package appsdk +package app import ( "context" - "fmt" "sync" "time" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" ) // ConfigUpdateProcessor contains the data need to process configuration updates type ConfigUpdateProcessor struct { - sdk *AppFunctionsSDK + svc *Service } -// NewConfigUpdateProcessor creates a new ConfigUpdateProcessor which process configuration updates triggered from +// NewConfigUpdateProcessor creates a new ConfigUpdateProcessor which processes configuration updates triggered from // the Configuration Provider -func NewConfigUpdateProcessor(sdk *AppFunctionsSDK) *ConfigUpdateProcessor { - return &ConfigUpdateProcessor{sdk: sdk} +func NewConfigUpdateProcessor(svc *Service) *ConfigUpdateProcessor { + return &ConfigUpdateProcessor{svc: svc} } // 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) + svc := processor.svc + svc.ctx.appWg.Add(1) go func() { - defer sdk.appWg.Done() + defer svc.ctx.appWg.Done() + lc := svc.LoggingClient() + lc.Info("Waiting for App Service configuration updates...") - sdk.LoggingClient.Info("Waiting for App Service configuration updates...") - - previousWriteable := sdk.config.Writable + previousWriteable := svc.config.Writable for { select { - case <-sdk.appCtx.Done(): - sdk.LoggingClient.Info("Exiting waiting for App Service configuration updates") + case <-svc.ctx.appCtx.Done(): + lc.Info("Exiting waiting for App Service configuration updates") return case <-configUpdated: - currentWritable := sdk.config.Writable - sdk.LoggingClient.Info("Processing App Service configuration updates") + currentWritable := svc.config.Writable + lc.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.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")) + lc.Warn("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)) + lc.Infof("StoreAndForward MaxRetryCount changed to %d", currentWritable.StoreAndForward.MaxRetryCount) case previousWriteable.StoreAndForward.RetryInterval != currentWritable.StoreAndForward.RetryInterval: if _, err := time.ParseDuration(currentWritable.StoreAndForward.RetryInterval); err != nil { - sdk.LoggingClient.Error(fmt.Sprintf("StoreAndForward RetryInterval not change: %s", err.Error())) + lc.Errorf("StoreAndForward RetryInterval not change: %s", err.Error()) currentWritable.StoreAndForward.RetryInterval = previousWriteable.StoreAndForward.RetryInterval continue } processor.processConfigChangedStoreForwardRetryInterval() - sdk.LoggingClient.Info( - fmt.Sprintf( - "StoreAndForward RetryInterval changed to %s", - currentWritable.StoreAndForward.RetryInterval)) + lc.Infof("StoreAndForward RetryInterval changed to %s", currentWritable.StoreAndForward.RetryInterval) case previousWriteable.StoreAndForward.Enabled != currentWritable.StoreAndForward.Enabled: processor.processConfigChangedStoreForwardEnabled() - sdk.LoggingClient.Info( - fmt.Sprintf( - "StoreAndForward Enabled changed to %v", - currentWritable.StoreAndForward.Enabled)) + lc.Infof("StoreAndForward Enabled changed to %v", currentWritable.StoreAndForward.Enabled) default: // Assume change is in the pipeline since all others have been checked appropriately @@ -106,9 +98,8 @@ func (processor *ConfigUpdateProcessor) WaitForConfigUpdates(configUpdated confi }() } -// processConfigChangedStoreForwardRetryInterval handles when the Store and Forward RetryInterval setting has been updated func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardRetryInterval() { - sdk := processor.sdk + sdk := processor.svc if sdk.config.Writable.StoreAndForward.Enabled { sdk.stopStoreForward() @@ -116,23 +107,30 @@ func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardRetryInt } } -// processConfigChangedStoreForwardEnabled handles when the Store and Forward Enabled setting has been updated func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardEnabled() { - sdk := processor.sdk + sdk := processor.svc if sdk.config.Writable.StoreAndForward.Enabled { + storeClient := container.StoreClientFrom(sdk.dic.Get) // StoreClient must be set up for StoreAndForward - if sdk.storeClient == nil { + if storeClient == nil { var err error - startupTimer := startup.NewStartUpTimer(sdk.ServiceKey) - sdk.storeClient, err = handlers.InitializeStoreClient(sdk.secretProvider, sdk.config, startupTimer, sdk.LoggingClient) + startupTimer := startup.NewStartUpTimer(sdk.serviceKey) + secretProvider := bootstrapContainer.SecretProviderFrom(sdk.dic.Get) + storeClient, err = handlers.InitializeStoreClient(secretProvider, sdk.config, startupTimer, sdk.LoggingClient()) if err != nil { // Error already logged sdk.config.Writable.StoreAndForward.Enabled = false return } - sdk.runtime.Initialize(sdk.storeClient, sdk.secretProvider) + sdk.dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return storeClient + }, + }) + + sdk.runtime.Initialize(sdk.dic) } sdk.startStoreForward() @@ -141,14 +139,13 @@ func (processor *ConfigUpdateProcessor) processConfigChangedStoreForwardEnabled( } } -// processConfigChangedPipeline handles when any of the Pipeline settings have been updated func (processor *ConfigUpdateProcessor) processConfigChangedPipeline() { - sdk := processor.sdk + sdk := processor.svc if sdk.usingConfigurablePipeline { transforms, err := sdk.LoadConfigurablePipeline() if err != nil { - sdk.LoggingClient.Error("unable to reload Configurable Pipeline from new configuration: " + err.Error()) + sdk.LoggingClient().Error("unable to reload Configurable Pipeline from new configuration: " + err.Error()) // Reset the transforms so error occurs when attempting to execute the pipeline. sdk.transforms = nil sdk.runtime.SetTransforms(nil) @@ -157,32 +154,23 @@ func (processor *ConfigUpdateProcessor) processConfigChangedPipeline() { err = sdk.SetFunctionsPipeline(transforms...) if err != nil { - sdk.LoggingClient.Error("unable to set Configurable Pipeline functions from new configuration: " + err.Error()) + sdk.LoggingClient().Error("unable to set Configurable Pipeline functions from new configuration: " + err.Error()) return } - sdk.LoggingClient.Info("Configurable Pipeline successfully reloaded from new configuration") + sdk.LoggingClient().Info("Configurable Pipeline successfully reloaded from new configuration") } } -// startStoreForward starts the Store and Forward processing -func (sdk *AppFunctionsSDK) startStoreForward() { +func (svc *Service) 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) + svc.ctx.storeForwardWg = &sync.WaitGroup{} + storeForwardEnabledCtx, svc.ctx.storeForwardCancelCtx = context.WithCancel(context.Background()) + svc.runtime.StartStoreAndForward(svc.ctx.appWg, svc.ctx.appCtx, svc.ctx.storeForwardWg, storeForwardEnabledCtx, svc.serviceKey) } -// 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() +func (svc *Service) stopStoreForward() { + svc.LoggingClient().Info("Canceling Store and Forward retry loop") + svc.ctx.storeForwardCancelCtx() + svc.ctx.storeForwardWg.Wait() } diff --git a/appsdk/configurable.go b/internal/app/configurable.go similarity index 73% rename from appsdk/configurable.go rename to internal/app/configurable.go index a5d69b54d..6b5701c56 100644 --- a/appsdk/configurable.go +++ b/internal/app/configurable.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,18 @@ // limitations under the License. // -package appsdk +package app import ( "fmt" "strconv" "strings" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" ) const ( @@ -81,10 +83,17 @@ type postPutParameters struct { secretName string } -// AppFunctionsSDKConfigurable contains the helper functions that return the function pointers for building the configurable function pipeline. +// Configurable contains the helper functions that return the function pointers for building the configurable function pipeline. // They transform the parameters map from the Pipeline configuration in to the actual actual parameters required by the function. -type AppFunctionsSDKConfigurable struct { - Sdk *AppFunctionsSDK +type Configurable struct { + lc logger.LoggingClient +} + +// NewConfigurable returns a new instance of Configurable +func NewConfigurable(lc logger.LoggingClient) *Configurable { + return &Configurable{ + lc: lc, + } } // FilterByProfileName - Specify the profile names of interest to filter for data coming from certain sensors. @@ -95,8 +104,8 @@ type AppFunctionsSDKConfigurable struct { // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByProfileName", parameters, ProfileNames) +func (app *Configurable) FilterByProfileName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByProfileName", parameters, ProfileNames) if !ok { return nil } @@ -112,8 +121,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByProfileName(parameters map[st // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByDeviceName", parameters, DeviceNames) +func (app *Configurable) FilterByDeviceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByDeviceName", parameters, DeviceNames) if !ok { return nil } @@ -129,8 +138,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByDeviceName(parameters map[str // event is received or if no data is received. // For example, data generated by a motor does not get passed to functions only interested in data from a thermostat. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterBySourceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterBySourceName", parameters, SourceNames) +func (app *Configurable) FilterBySourceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterBySourceName", parameters, SourceNames) if !ok { return nil } @@ -146,8 +155,8 @@ func (dynamic AppFunctionsSDKConfigurable) FilterBySourceName(parameters map[str // event is received or if no data is received. // For example, pressure reading data does not go to functions only interested in motion data. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) FilterByResourceName(parameters map[string]string) appcontext.AppFunction { - transform, ok := dynamic.processFilterParameters("FilterByResourceName", parameters, ResourceNames) +func (app *Configurable) FilterByResourceName(parameters map[string]string) interfaces.AppFunction { + transform, ok := app.processFilterParameters("FilterByResourceName", parameters, ResourceNames) if !ok { return nil } @@ -158,10 +167,10 @@ func (dynamic AppFunctionsSDKConfigurable) FilterByResourceName(parameters map[s // Transform transforms an EdgeX event to XML or JSON based on specified transform type. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Transform(parameters map[string]string) interfaces.AppFunction { transformType, ok := parameters[TransformType] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Transform", TransformType) + app.lc.Errorf("Could not find '%s' parameter for Transform", TransformType) return nil } @@ -173,7 +182,7 @@ func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]strin case TransformJson: return transform.TransformToJSON default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid transform type '%s'. Must be '%s' or '%s'", transformType, TransformXml, @@ -185,15 +194,15 @@ func (dynamic AppFunctionsSDKConfigurable) Transform(parameters map[string]strin // PushToCore pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in // CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) PushToCore(parameters map[string]string) interfaces.AppFunction { deviceName, ok := parameters[DeviceName] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + DeviceName) + app.lc.Error("Could not find " + DeviceName) return nil } readingName, ok := parameters[ReadingName] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + readingName) + app.lc.Error("Could not find " + readingName) return nil } deviceName = strings.TrimSpace(deviceName) @@ -208,10 +217,10 @@ func (dynamic AppFunctionsSDKConfigurable) PushToCore(parameters map[string]stri // Compress compresses data received as either a string,[]byte, or json.Marshaller using the specified algorithm (GZIP or ZLIB) // and returns a base64 encoded string as a []byte. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Compress(parameters map[string]string) interfaces.AppFunction { algorithm, ok := parameters[Algorithm] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Compress", Algorithm) + app.lc.Errorf("Could not find '%s' parameter for Compress", Algorithm) return nil } @@ -223,7 +232,7 @@ func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string case CompressZLIB: return transform.CompressWithZLIB default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid compression algorithm '%s'. Must be '%s' or '%s'", algorithm, CompressGZIP, @@ -235,10 +244,10 @@ func (dynamic AppFunctionsSDKConfigurable) Compress(parameters map[string]string // Encrypt encrypts either a string, []byte, or json.Marshaller type using specified encryption // algorithm (AES only at this time). It will return a byte[] of the encrypted data. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Encrypt(parameters map[string]string) interfaces.AppFunction { algorithm, ok := parameters[Algorithm] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Encrypt", Algorithm) + app.lc.Errorf("Could not find '%s' parameter for Encrypt", Algorithm) return nil } @@ -251,19 +260,19 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) // If EncryptionKey not specified, then SecretPath & SecretName must be specified if len(encryptionKey) == 0 && (len(secretPath) == 0 || len(secretName) == 0) { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' or '%s' and '%s' in configuration", EncryptionKey, SecretPath, SecretName) + app.lc.Errorf("Could not find '%s' or '%s' and '%s' in configuration", EncryptionKey, SecretPath, SecretName) return nil } // SecretPath & SecretName both must be specified it one of them is. if (len(secretPath) != 0 && len(secretName) == 0) || (len(secretPath) == 0 && len(secretName) != 0) { - dynamic.Sdk.LoggingClient.Errorf("'%s' and '%s' both must be set in configuration", SecretPath, SecretName) + app.lc.Errorf("'%s' and '%s' both must be set in configuration", SecretPath, SecretName) return nil } initVector, ok := parameters[InitVector] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + InitVector) + app.lc.Error("Could not find " + InitVector) return nil } @@ -278,7 +287,7 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) case EncryptAES: return transform.EncryptWithAES default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid encryption algorithm '%s'. Must be '%s'", algorithm, EncryptAES) @@ -290,10 +299,10 @@ func (dynamic AppFunctionsSDKConfigurable) Encrypt(parameters map[string]string) // then the event that triggered the pipeline will be used. Passing an empty string to the mimetype // method will default to application/json. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]string) appcontext.AppFunction { - params, err := dynamic.processHttpExportParameters(parameters) +func (app *Configurable) HTTPExport(parameters map[string]string) interfaces.AppFunction { + params, err := app.processHttpExportParameters(parameters) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) return nil } @@ -316,7 +325,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]stri case ExportMethodPut: return transform.HTTPPut default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid HTTPExport method of '%s'. Must be '%s' or '%s'", params.method, ExportMethodPost, @@ -329,7 +338,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPExport(parameters map[string]stri // MQTTExport will send data from the previous function to the specified Endpoint via MQTT publish. If no previous function exists, // then the event that triggered the pipeline will be used. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) MQTTExport(parameters map[string]string) interfaces.AppFunction { var err error qos := 0 retain := false @@ -338,35 +347,35 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri brokerAddress, ok := parameters[BrokerAddress] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BrokerAddress) + app.lc.Error("Could not find " + BrokerAddress) return nil } topic, ok := parameters[Topic] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + Topic) + app.lc.Error("Could not find " + Topic) return nil } secretPath, ok := parameters[SecretPath] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + SecretPath) + app.lc.Error("Could not find " + SecretPath) return nil } authMode, ok := parameters[AuthMode] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + AuthMode) + app.lc.Error("Could not find " + AuthMode) return nil } clientID, ok := parameters[ClientID] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + ClientID) + app.lc.Error("Could not find " + ClientID) return nil } qosVal, ok := parameters[Qos] if ok { qos, err = strconv.Atoi(qosVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + Qos + " value") + app.lc.Error("Unable to parse " + Qos + " value") return nil } } @@ -374,7 +383,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { retain, err = strconv.ParseBool(retainVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + Retain + " value") + app.lc.Error("Unable to parse " + Retain + " value") return nil } } @@ -382,7 +391,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { autoReconnect, err = strconv.ParseBool(autoreconnectVal) if err != nil { - dynamic.Sdk.LoggingClient.Error("Unable to parse " + AutoReconnect + " value") + app.lc.Error("Unable to parse " + AutoReconnect + " value") return nil } } @@ -390,7 +399,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { skipCertVerify, err = strconv.ParseBool(skipVerifyVal) if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", skipVerifyVal, SkipVerify), "error", err) + app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", skipVerifyVal, SkipVerify), "error", err) return nil } } @@ -411,7 +420,7 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri if ok { persistOnError, err = strconv.ParseBool(value) if err != nil { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err) + app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err) return nil } } @@ -419,27 +428,28 @@ func (dynamic AppFunctionsSDKConfigurable) MQTTExport(parameters map[string]stri return transform.MQTTSend } -// SetOutputData sets the output data to that passed in from the previous function. -// It will return an error and stop the pipeline if data passed in is not of type []byte, string or json.Marshaller +// SetResponseData sets the response data to that passed in from the previous function and the response content type +// to that set in the ResponseContentType configuration parameter. It will return an error and stop the pipeline if +// data passed in is not of type []byte, string or json.Marshaller // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) SetOutputData(parameters map[string]string) appcontext.AppFunction { - transform := transforms.OutputData{} +func (app *Configurable) SetResponseData(parameters map[string]string) interfaces.AppFunction { + transform := transforms.ResponseData{} value, ok := parameters[ResponseContentType] if ok && len(value) > 0 { transform.ResponseContentType = value } - return transform.SetOutputData + return transform.SetResponseData } // Batch sets up Batching of events based on the specified mode parameter (BatchByCount, BatchByTime or BatchByTimeAndCount) // and mode specific parameters. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) Batch(parameters map[string]string) interfaces.AppFunction { mode, ok := parameters[Mode] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for Batch", Mode) + app.lc.Errorf("Could not find '%s' parameter for Batch", Mode) return nil } @@ -447,13 +457,13 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a case BatchByCount: batchThreshold, ok := parameters[BatchThreshold] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByCount", BatchThreshold) + app.lc.Errorf("Could not find '%s' parameter for BatchByCount", BatchThreshold) return nil } thresholdValue, err := strconv.Atoi(batchThreshold) if err != nil { - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Could not parse '%s' to an int for '%s' parameter for BatchByCount: %s", batchThreshold, BatchThreshold, err.Error()) return nil @@ -461,46 +471,46 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a transform, err := transforms.NewBatchByCount(thresholdValue) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch case BatchByTime: timeInterval, ok := parameters[TimeInterval] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for BatchByTime", TimeInterval) + app.lc.Errorf("Could not find '%s' parameter for BatchByTime", TimeInterval) return nil } transform, err := transforms.NewBatchByTime(timeInterval) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch case BatchByTimeAndCount: timeInterval, ok := parameters[TimeInterval] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + TimeInterval) + app.lc.Error("Could not find " + TimeInterval) return nil } batchThreshold, ok := parameters[BatchThreshold] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + BatchThreshold) + app.lc.Error("Could not find " + BatchThreshold) return nil } thresholdValue, err := strconv.Atoi(batchThreshold) if err != nil { - dynamic.Sdk.LoggingClient.Errorf("Could not parse '%s' to an int for '%s' parameter: %s", batchThreshold, BatchThreshold, err.Error()) + app.lc.Errorf("Could not parse '%s' to an int for '%s' parameter: %s", batchThreshold, BatchThreshold, err.Error()) } transform, err := transforms.NewBatchByTimeAndCount(timeInterval, thresholdValue) if err != nil { - dynamic.Sdk.LoggingClient.Error(err.Error()) + app.lc.Error(err.Error()) } return transform.Batch default: - dynamic.Sdk.LoggingClient.Errorf( + app.lc.Errorf( "Invalid batch mode '%s'. Must be '%s', '%s' or '%s'", mode, BatchByCount, @@ -511,10 +521,10 @@ func (dynamic AppFunctionsSDKConfigurable) Batch(parameters map[string]string) a } // JSONLogic ... -func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) JSONLogic(parameters map[string]string) interfaces.AppFunction { rule, ok := parameters[Rule] if !ok { - dynamic.Sdk.LoggingClient.Error("Could not find " + Rule) + app.lc.Error("Could not find " + Rule) return nil } @@ -524,10 +534,10 @@ func (dynamic AppFunctionsSDKConfigurable) JSONLogic(parameters map[string]strin // AddTags adds the configured list of tags to Events passed to the transform. // This function is a configuration function and returns a function pointer. -func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) appcontext.AppFunction { +func (app *Configurable) AddTags(parameters map[string]string) interfaces.AppFunction { tagsSpec, ok := parameters[Tags] if !ok { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not find '%s' parameter", Tags)) + app.lc.Error(fmt.Sprintf("Could not find '%s' parameter", Tags)) return nil } @@ -537,16 +547,16 @@ func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) for _, tag := range tagKeyValues { keyValue := util.DeleteEmptyAndTrim(strings.FieldsFunc(tag, util.SplitColon)) if len(keyValue) != 2 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec)) + app.lc.Error(fmt.Sprintf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec)) return nil } if len(keyValue[0]) == 0 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Tag key missing. Got '%s'", tag)) + app.lc.Error(fmt.Sprintf("Tag key missing. Got '%s'", tag)) return nil } if len(keyValue[1]) == 0 { - dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Tag value missing. Got '%s'", tag)) + app.lc.Error(fmt.Sprintf("Tag value missing. Got '%s'", tag)) return nil } @@ -557,13 +567,13 @@ func (dynamic AppFunctionsSDKConfigurable) AddTags(parameters map[string]string) return transform.AddTags } -func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( +func (app *Configurable) processFilterParameters( funcName string, parameters map[string]string, paramName string) (*transforms.Filter, bool) { names, ok := parameters[paramName] if !ok { - dynamic.Sdk.LoggingClient.Errorf("Could not find '%s' parameter for %s", paramName, funcName) + app.lc.Errorf("Could not find '%s' parameter for %s", paramName, funcName) return nil, false } @@ -573,7 +583,7 @@ func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( var err error filterOutBool, err = strconv.ParseBool(filterOut) if err != nil { - dynamic.Sdk.LoggingClient.Errorf("Could not convert filterOut value `%s` to bool for %s", filterOut, funcName) + app.lc.Errorf("Could not convert filterOut value `%s` to bool for %s", filterOut, funcName) return nil, false } } @@ -587,7 +597,7 @@ func (dynamic AppFunctionsSDKConfigurable) processFilterParameters( return &transform, true } -func (dynamic AppFunctionsSDKConfigurable) processHttpExportParameters( +func (app *Configurable) processHttpExportParameters( parameters map[string]string) (*postPutParameters, error) { result := postPutParameters{} diff --git a/appsdk/configurable_test.go b/internal/app/configurable_test.go similarity index 88% rename from appsdk/configurable_test.go rename to internal/app/configurable_test.go index 32d1d7ba5..0804bedbc 100644 --- a/appsdk/configurable_test.go +++ b/internal/app/configurable_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package appsdk +package app import ( "net/http" @@ -24,11 +24,7 @@ import ( ) func TestFilterByProfileName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -54,11 +50,7 @@ func TestFilterByProfileName(t *testing.T) { } func TestFilterByDeviceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -84,11 +76,7 @@ func TestFilterByDeviceName(t *testing.T) { } func TestFilterBySourceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -114,11 +102,7 @@ func TestFilterBySourceName(t *testing.T) { } func TestFilterByResourceName(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -144,11 +128,7 @@ func TestFilterByResourceName(t *testing.T) { } func TestTransform(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { Name string @@ -171,11 +151,7 @@ func TestTransform(t *testing.T) { } func TestHTTPExport(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} testUrl := "http://url" testMimeType := clients.ContentTypeJSON @@ -254,11 +230,7 @@ func TestHTTPExport(t *testing.T) { } func TestSetOutputData(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { name string @@ -272,22 +244,18 @@ func TestSetOutputData(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - trx := configurable.SetOutputData(tt.params) + trx := configurable.SetResponseData(tt.params) if tt.expectNil { - assert.Nil(t, trx, "return result from SetOutputData should be nil") + assert.Nil(t, trx, "return result from SetResponseData should be nil") } else { - assert.NotNil(t, trx, "return result from SetOutputData should not be nil") + assert.NotNil(t, trx, "return result from SetResponseData should not be nil") } }) } } func TestBatchByCount(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByCount @@ -297,11 +265,7 @@ func TestBatchByCount(t *testing.T) { } func TestBatchByTime(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByTime @@ -311,11 +275,7 @@ func TestBatchByTime(t *testing.T) { } func TestBatchByTimeAndCount(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[Mode] = BatchByTimeAndCount @@ -330,22 +290,15 @@ func TestJSONLogic(t *testing.T) { params := make(map[string]string) params[Rule] = "{}" - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} + trx := configurable.JSONLogic(params) assert.NotNil(t, trx, "return result from JSONLogic should not be nil") } func TestMQTTExport(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} params := make(map[string]string) params[BrokerAddress] = "mqtt://broker:8883" @@ -364,11 +317,7 @@ func TestMQTTExport(t *testing.T) { } func TestAddTags(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} tests := []struct { Name string @@ -397,11 +346,7 @@ func TestAddTags(t *testing.T) { } func TestEncrypt(t *testing.T) { - configurable := AppFunctionsSDKConfigurable{ - Sdk: &AppFunctionsSDK{ - LoggingClient: lc, - }, - } + configurable := Configurable{lc: lc} key := "xyz12345" vector := "1243565" diff --git a/internal/app/service.go b/internal/app/service.go new file mode 100644 index 000000000..936d61c3e --- /dev/null +++ b/internal/app/service.go @@ -0,0 +1,532 @@ +// +// Copyright (c) 2021 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 app + +import ( + "context" + "errors" + "fmt" + nethttp "net/http" + "os" + "os/signal" + "reflect" + "strings" + "sync" + "syscall" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/secret" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-registry/v2/registry" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/config" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/flags" + bootstrapHandlers "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/handlers" + bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/edgexfoundry/go-mod-messaging/v2/messaging" + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + "github.com/gorilla/mux" +) + +const ( + envProfile = "EDGEX_PROFILE" + envServiceKey = "EDGEX_SERVICE_KEY" + + optionalPasswordKey = "Password" +) + +// NewService create, initializes and returns new instance of app.Service which implements the +// interfaces.ApplicationService interface +func NewService(serviceKey string, targetType interface{}, profileSuffixPlaceholder string) *Service { + return &Service{ + serviceKey: serviceKey, + targetType: targetType, + profileSuffixPlaceholder: profileSuffixPlaceholder, + } +} + +// Service provides the necessary struct and functions to create an instance of the +// interfaces.ApplicationService interface. +type Service struct { + dic *di.Container + serviceKey string + targetType interface{} + config *common.ConfigurationStruct + lc logger.LoggingClient + transforms []interfaces.AppFunction + usingConfigurablePipeline bool + runtime *runtime.GolangRuntime + webserver *webserver.WebServer + ctx contextGroup + deferredFunctions []bootstrap.Deferred + backgroundPublishChannel <-chan types.MessageEnvelope + customTriggerFactories map[string]func(sdk *Service) (interfaces.Trigger, error) + profileSuffixPlaceholder string + commandLine commandLineFlags +} + +type commandLineFlags struct { + skipVersionCheck bool + serviceKeyOverride string +} + +type contextGroup struct { + storeForwardWg *sync.WaitGroup + storeForwardCancelCtx context.CancelFunc + appWg *sync.WaitGroup + appCtx context.Context + appCancelCtx context.CancelFunc + stop context.CancelFunc +} + +// AddRoute allows you to leverage the existing webserver to add routes. +func (svc *Service) AddRoute(route string, handler func(nethttp.ResponseWriter, *nethttp.Request), methods ...string) error { + if route == clients.ApiPingRoute || + route == clients.ApiConfigRoute || + route == clients.ApiMetricsRoute || + route == clients.ApiVersionRoute || + route == internal.ApiTriggerRoute { + return errors.New("route is reserved") + } + return svc.webserver.AddRoute(route, svc.addContext(handler), methods...) +} + +// AddBackgroundPublisher will create a channel of provided capacity to be +// consumed by the MessageBus output and return a publisher that writes to it +func (svc *Service) AddBackgroundPublisher(capacity int) interfaces.BackgroundPublisher { + bgchan, pub := newBackgroundPublisher(capacity) + svc.backgroundPublishChannel = bgchan + return pub +} + +// MakeItStop will force the service loop to exit in the same fashion as SIGINT/SIGTERM received from the OS +func (svc *Service) MakeItStop() { + if svc.ctx.stop != nil { + svc.ctx.stop() + } else { + svc.lc.Warn("MakeItStop called but no stop handler set on SDK - is the service running?") + } +} + +// MakeItRun initializes and starts the trigger as specified in the +// configuration. It will also configure the webserver and start listening on +// the specified port. +func (svc *Service) MakeItRun() error { + runCtx, stop := context.WithCancel(context.Background()) + + svc.ctx.stop = stop + + svc.runtime = &runtime.GolangRuntime{ + TargetType: svc.targetType, + ServiceKey: svc.serviceKey, + } + + svc.runtime.Initialize(svc.dic) + svc.runtime.SetTransforms(svc.transforms) + + // determine input type and create trigger for it + t := svc.setupTrigger(svc.config, svc.runtime) + if t == nil { + return errors.New("Failed to create Trigger") + } + + // Initialize the trigger (i.e. start a web server, or connect to message bus) + deferred, err := t.Initialize(svc.ctx.appWg, svc.ctx.appCtx, svc.backgroundPublishChannel) + if err != nil { + svc.lc.Error(err.Error()) + return errors.New("Failed to initialize Trigger") + } + + // deferred is a a function that needs to be called when services exits. + svc.addDeferred(deferred) + + if svc.config.Writable.StoreAndForward.Enabled { + svc.startStoreForward() + } else { + svc.lc.Info("StoreAndForward disabled. Not running retry loop.") + } + + svc.lc.Info(svc.config.Service.StartupMsg) + + signals := make(chan os.Signal) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + httpErrors := make(chan error) + defer close(httpErrors) + + svc.webserver.StartWebServer(httpErrors) + + select { + case httpError := <-httpErrors: + svc.lc.Info("Http error received: ", httpError.Error()) + err = httpError + + case signalReceived := <-signals: + svc.lc.Info("Terminating signal received: " + signalReceived.String()) + + case <-runCtx.Done(): + svc.lc.Info("Terminating: svc.MakeItStop called") + } + + svc.ctx.stop = nil + + if svc.config.Writable.StoreAndForward.Enabled { + svc.ctx.storeForwardCancelCtx() + svc.ctx.storeForwardWg.Wait() + } + + svc.ctx.appCancelCtx() // Cancel all long running go funcs + svc.ctx.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 svc.deferredFunctions { + deferredFunc() + } + + return err +} + +// LoadConfigurablePipeline sets the function pipeline from configuration +func (svc *Service) LoadConfigurablePipeline() ([]interfaces.AppFunction, error) { + var pipeline []interfaces.AppFunction + + svc.usingConfigurablePipeline = true + + svc.targetType = nil + + if svc.config.Writable.Pipeline.UseTargetTypeOfByteArray { + svc.targetType = &[]byte{} + } + + configurable := NewConfigurable(svc.lc) + + valueOfType := reflect.ValueOf(configurable) + pipelineConfig := svc.config.Writable.Pipeline + executionOrder := util.DeleteEmptyAndTrim(strings.FieldsFunc(pipelineConfig.ExecutionOrder, util.SplitComma)) + + if len(executionOrder) <= 0 { + return nil, errors.New( + "execution Order has 0 functions specified. You must have a least one function in the pipeline") + } + + svc.lc.Debugf("Function Pipeline Execution Order: [%s]", pipelineConfig.ExecutionOrder) + + for _, functionName := range executionOrder { + functionName = strings.TrimSpace(functionName) + configuration, ok := pipelineConfig.Functions[functionName] + if !ok { + return nil, fmt.Errorf("function '%s' configuration not found in Pipeline.Functions section", functionName) + } + + result := valueOfType.MethodByName(functionName) + if result.Kind() == reflect.Invalid { + return nil, fmt.Errorf("function %s is not a built in SDK function", functionName) + } else if result.IsNil() { + return nil, fmt.Errorf("invalid/missing configuration for %s", functionName) + } + + // determine number of parameters required for function call + inputParameters := make([]reflect.Value, result.Type().NumIn()) + // set keys to be all lowercase to avoid casing issues from configuration + for key := range configuration.Parameters { + value := configuration.Parameters[key] + delete(configuration.Parameters, key) // Make sure the old key has been removed so don't have multiples + configuration.Parameters[strings.ToLower(key)] = value + } + for index := range inputParameters { + parameter := result.Type().In(index) + + switch parameter { + case reflect.TypeOf(map[string]string{}): + inputParameters[index] = reflect.ValueOf(configuration.Parameters) + + default: + return nil, fmt.Errorf( + "function %s has an unsupported parameter type: %s", + functionName, + parameter.String(), + ) + } + } + + function, ok := result.Call(inputParameters)[0].Interface().(interfaces.AppFunction) + if !ok { + return nil, fmt.Errorf("failed to cast function %s as AppFunction type", functionName) + } + + if function == nil { + return nil, fmt.Errorf("%s from configuration failed", functionName) + } + + pipeline = append(pipeline, function) + svc.lc.Debugf( + "%s function added to configurable pipeline with parameters: [%s]", + functionName, + listParameters(configuration.Parameters)) + } + + return pipeline, nil +} + +// SetFunctionsPipeline sets the function pipeline to the list of specified functions in the order provided. +func (svc *Service) SetFunctionsPipeline(transforms ...interfaces.AppFunction) error { + if len(transforms) == 0 { + return errors.New("no transforms provided to pipeline") + } + + svc.transforms = transforms + + if svc.runtime != nil { + svc.runtime.SetTransforms(transforms) + svc.runtime.TargetType = svc.targetType + } + + return nil +} + +// ApplicationSettings returns the values specified in the custom configuration section. +func (svc *Service) ApplicationSettings() map[string]string { + return svc.config.ApplicationSettings +} + +// GetAppSettingStrings returns the strings slice for the specified App Setting. +func (svc *Service) GetAppSettingStrings(setting string) ([]string, error) { + if svc.config.ApplicationSettings == nil { + return nil, fmt.Errorf("%s setting not found: ApplicationSettings section is missing", setting) + } + + settingValue, ok := svc.config.ApplicationSettings[setting] + if !ok { + return nil, fmt.Errorf("%s setting not found in ApplicationSettings", setting) + } + + valueStrings := util.DeleteEmptyAndTrim(strings.FieldsFunc(settingValue, util.SplitComma)) + + return valueStrings, nil +} + +// Initialize bootstraps the service making it ready to accept functions for the pipeline and to run the configured trigger. +func (svc *Service) Initialize() error { + startupTimer := startup.NewStartUpTimer(svc.serviceKey) + + additionalUsage := + " -s/--skipVersionCheck Indicates the service should skip the Core Service's version compatibility check.\n" + + " -sk/--serviceKey Overrides the service service key used with Registry and/or Configuration Providers.\n" + + " If the name provided contains the text ``, this text will be replaced with\n" + + " the name of the profile used." + + sdkFlags := flags.NewWithUsage(additionalUsage) + sdkFlags.FlagSet.BoolVar(&svc.commandLine.skipVersionCheck, "skipVersionCheck", false, "") + sdkFlags.FlagSet.BoolVar(&svc.commandLine.skipVersionCheck, "s", false, "") + sdkFlags.FlagSet.StringVar(&svc.commandLine.serviceKeyOverride, "serviceKey", "", "") + sdkFlags.FlagSet.StringVar(&svc.commandLine.serviceKeyOverride, "sk", "", "") + + sdkFlags.Parse(os.Args[1:]) + + // Temporarily setup logging to STDOUT so the client can be used before bootstrapping is completed + svc.lc = logger.NewClient(svc.serviceKey, models.InfoLog) + + svc.setServiceKey(sdkFlags.Profile()) + + svc.lc.Info(fmt.Sprintf("Starting %s %s ", svc.serviceKey, internal.ApplicationVersion)) + + svc.config = &common.ConfigurationStruct{} + svc.dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return svc.config + }, + }) + + svc.ctx.appCtx, svc.ctx.appCancelCtx = context.WithCancel(context.Background()) + svc.ctx.appWg = &sync.WaitGroup{} + + var deferred bootstrap.Deferred + var successful bool + var configUpdated config.UpdatedStream = make(chan struct{}) + + svc.ctx.appWg, deferred, successful = bootstrap.RunAndReturnWaitGroup( + svc.ctx.appCtx, + svc.ctx.appCancelCtx, + sdkFlags, + svc.serviceKey, + internal.ConfigRegistryStem, + svc.config, + configUpdated, + startupTimer, + svc.dic, + []bootstrapInterfaces.BootstrapHandler{ + bootstrapHandlers.SecureProviderBootstrapHandler, + handlers.NewDatabase().BootstrapHandler, + handlers.NewClients().BootstrapHandler, + handlers.NewTelemetry().BootstrapHandler, + handlers.NewVersionValidator(svc.commandLine.skipVersionCheck, internal.SDKVersion).BootstrapHandler, + }, + ) + + // deferred is a a function that needs to be called when services exits. + svc.addDeferred(deferred) + + if !successful { + return fmt.Errorf("boostrapping failed") + } + + // Bootstrapping is complete, so now need to retrieve the needed objects from the containers. + svc.lc = bootstrapContainer.LoggingClientFrom(svc.dic.Get) + + // If using the RedisStreams MessageBus implementation then need to make sure the + // password for the Redis DB is set in the MessageBus Optional properties. + triggerType := strings.ToUpper(svc.config.Trigger.Type) + if triggerType == TriggerTypeMessageBus && + svc.config.Trigger.EdgexMessageBus.Type == messaging.RedisStreams { + + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + credentials, err := secretProvider.GetSecrets(svc.config.Database.Type) + if err != nil { + return fmt.Errorf("unable to set RedisStreams password from DB credentials") + } + svc.config.Trigger.EdgexMessageBus.Optional[optionalPasswordKey] = credentials[secret.PasswordKey] + } + + // 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(svc).WaitForConfigUpdates(configUpdated) + + svc.webserver = webserver.NewWebServer(svc.dic, mux.NewRouter()) + svc.webserver.ConfigureStandardRoutes() + + svc.lc.Info("Service started in: " + startupTimer.SinceAsString()) + + return nil +} + +// GetSecret retrieves secret data from the secret store at the specified path. +func (svc *Service) GetSecret(path string, keys ...string) (map[string]string, error) { + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + return secretProvider.GetSecrets(path, keys...) +} + +// StoreSecret stores the secret data to a secret store at the specified path. +func (svc *Service) StoreSecret(path string, secretData map[string]string) error { + secretProvider := bootstrapContainer.SecretProviderFrom(svc.dic.Get) + return secretProvider.StoreSecrets(path, secretData) +} + +// LoggingClient returns the Logging client from the dependency injection container +func (svc *Service) LoggingClient() logger.LoggingClient { + return svc.lc +} + +// RegistryClient returns the Registry client, which may be nil, from the dependency injection container +func (svc *Service) RegistryClient() registry.Client { + return bootstrapContainer.RegistryFrom(svc.dic.Get) +} + +// EventClient returns the Event client, which may be nil, from the dependency injection container +func (svc *Service) EventClient() coredata.EventClient { + return container.EventClientFrom(svc.dic.Get) +} + +// CommandClient returns the Command client, which may be nil, from the dependency injection container +func (svc *Service) CommandClient() command.CommandClient { + return container.CommandClientFrom(svc.dic.Get) +} + +// NotificationsClient returns the Notifications client, which may be nil, from the dependency injection container +func (svc *Service) NotificationsClient() notifications.NotificationsClient { + return container.NotificationsClientFrom(svc.dic.Get) +} + +func listParameters(parameters map[string]string) string { + result := "" + first := true + for key, value := range parameters { + if first { + result = fmt.Sprintf("%s='%s'", key, value) + first = false + continue + } + + result += fmt.Sprintf(", %s='%s'", key, value) + } + + return result +} + +func (svc *Service) addContext(next func(nethttp.ResponseWriter, *nethttp.Request)) func(nethttp.ResponseWriter, *nethttp.Request) { + return func(w nethttp.ResponseWriter, r *nethttp.Request) { + ctx := context.WithValue(r.Context(), interfaces.AppServiceContextKey, svc) + next(w, r.WithContext(ctx)) + } +} + +func (svc *Service) addDeferred(deferred bootstrap.Deferred) { + if deferred != nil { + svc.deferredFunctions = append(svc.deferredFunctions, deferred) + } +} + +func (svc *Service) setServiceKey(profile string) { + envValue := os.Getenv(envServiceKey) + if len(envValue) > 0 { + svc.commandLine.serviceKeyOverride = envValue + svc.lc.Info( + fmt.Sprintf("Environment profileOverride of '-n/--serviceName' by environment variable: %s=%s", + envServiceKey, + envValue)) + } + + // serviceKeyOverride may have been set by the -n/--serviceName command-line option and not the environment variable + if len(svc.commandLine.serviceKeyOverride) > 0 { + svc.serviceKey = svc.commandLine.serviceKeyOverride + } + + if !strings.Contains(svc.serviceKey, svc.profileSuffixPlaceholder) { + // No placeholder, so nothing to do here + return + } + + // Have to handle environment override here before common bootstrap is used so it is passed the proper service key + profileOverride := os.Getenv(envProfile) + if len(profileOverride) > 0 { + profile = profileOverride + } + + if len(profile) > 0 { + svc.serviceKey = strings.Replace(svc.serviceKey, svc.profileSuffixPlaceholder, profile, 1) + return + } + + // No profile specified so remove the placeholder text + svc.serviceKey = strings.Replace(svc.serviceKey, svc.profileSuffixPlaceholder, "", 1) +} diff --git a/appsdk/sdk_test.go b/internal/app/service_test.go similarity index 80% rename from appsdk/sdk_test.go rename to internal/app/service_test.go index f828e4d77..e1ad9d310 100644 --- a/appsdk/sdk_test.go +++ b/internal/app/service_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // limitations under the License. // -package appsdk +package app import ( "fmt" @@ -23,13 +23,16 @@ import ( "reflect" "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" triggerHttp "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/gorilla/mux" @@ -38,10 +41,20 @@ import ( ) var lc logger.LoggingClient +var dic *di.Container func TestMain(m *testing.M) { // No remote and no file results in STDOUT logging only lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + }) + m.Run() } @@ -51,9 +64,10 @@ func IsInstanceOf(objectPtr, typePtr interface{}) bool { func TestAddRoute(t *testing.T) { router := mux.NewRouter() - ws := webserver.NewWebServer(&common.ConfigurationStruct{}, nil, lc, router) - sdk := AppFunctionsSDK{ + ws := webserver.NewWebServer(dic, router) + + sdk := Service{ webserver: ws, } _ = sdk.AddRoute("/test", func(http.ResponseWriter, *http.Request) {}, http.MethodGet) @@ -69,7 +83,7 @@ func TestAddRoute(t *testing.T) { } func TestAddBackgroundPublisher(t *testing.T) { - sdk := AppFunctionsSDK{} + sdk := Service{} pub, ok := sdk.AddBackgroundPublisher(1).(*backgroundPublisher) if !ok { @@ -77,16 +91,16 @@ func TestAddBackgroundPublisher(t *testing.T) { } require.NotNil(t, pub.output, "publisher should have an output channel set") - require.NotNil(t, sdk.backgroundChannel, "sdk should have a background channel set for passing to trigger initialization") + require.NotNil(t, sdk.backgroundPublishChannel, "svc should have a background channel set for passing to trigger initialization") // compare addresses since types will not match - assert.Equal(t, fmt.Sprintf("%p", sdk.backgroundChannel), fmt.Sprintf("%p", pub.output), + assert.Equal(t, fmt.Sprintf("%p", sdk.backgroundPublishChannel), fmt.Sprintf("%p", pub.output), "same channel should be referenced by the BackgroundPublisher and the SDK.") } func TestSetupHTTPTrigger(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: "htTp", @@ -94,7 +108,7 @@ func TestSetupHTTPTrigger(t *testing.T) { }, } testRuntime := &runtime.GolangRuntime{} - testRuntime.Initialize(nil, nil) + testRuntime.Initialize(dic) testRuntime.SetTransforms(sdk.transforms) trigger := sdk.setupTrigger(sdk.config, testRuntime) result := IsInstanceOf(trigger, (*triggerHttp.Trigger)(nil)) @@ -102,8 +116,8 @@ func TestSetupHTTPTrigger(t *testing.T) { } func TestSetupMessageBusTrigger(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, @@ -111,7 +125,7 @@ func TestSetupMessageBusTrigger(t *testing.T) { }, } testRuntime := &runtime.GolangRuntime{} - testRuntime.Initialize(nil, nil) + testRuntime.Initialize(dic) testRuntime.SetTransforms(sdk.transforms) trigger := sdk.setupTrigger(sdk.config, testRuntime) result := IsInstanceOf(trigger, (*messagebus.Trigger)(nil)) @@ -119,8 +133,8 @@ func TestSetupMessageBusTrigger(t *testing.T) { } func TestSetFunctionsPipelineNoTransforms(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, @@ -133,20 +147,20 @@ func TestSetFunctionsPipelineNoTransforms(t *testing.T) { } func TestSetFunctionsPipelineOneTransform(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - runtime: &runtime.GolangRuntime{}, + sdk := Service{ + lc: lc, + runtime: &runtime.GolangRuntime{}, config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, }, }, } - function := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + function := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return true, nil } - sdk.runtime.Initialize(nil, nil) + sdk.runtime.Initialize(dic) err := sdk.SetFunctionsPipeline(function) require.NoError(t, err) assert.Equal(t, 1, len(sdk.transforms)) @@ -156,7 +170,7 @@ func TestApplicationSettings(t *testing.T) { expectedSettingKey := "ApplicationName" expectedSettingValue := "simple-filter-xml" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "ApplicationName": "simple-filter-xml", @@ -173,7 +187,7 @@ func TestApplicationSettings(t *testing.T) { } func TestApplicationSettingsNil(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{}, } @@ -185,7 +199,7 @@ func TestGetAppSettingStrings(t *testing.T) { setting := "DeviceNames" expected := []string{"dev1", "dev2"} - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{ "DeviceNames": "dev1, dev2", @@ -202,7 +216,7 @@ func TestGetAppSettingStringsSettingMissing(t *testing.T) { setting := "DeviceNames" expected := "setting not found in ApplicationSettings" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ ApplicationSettings: map[string]string{}, }, @@ -217,7 +231,7 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { setting := "DeviceNames" expected := "ApplicationSettings section is missing" - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{}, } @@ -227,8 +241,8 @@ func TestGetAppSettingStringsNoAppSettings(t *testing.T) { } func TestLoadConfigurablePipelineFunctionNotFound(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ @@ -249,8 +263,8 @@ func TestLoadConfigurablePipelineNotABuiltInSdkFunction(t *testing.T) { functions := make(map[string]common.PipelineFunction) functions["Bogus"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ @@ -275,14 +289,14 @@ func TestLoadConfigurablePipelineNumFunctions(t *testing.T) { functions["Transform"] = common.PipelineFunction{ Parameters: map[string]string{TransformType: TransformXml}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "FilterByDeviceName, Transform, SetOutputData", + ExecutionOrder: "FilterByDeviceName, Transform, SetResponseData", Functions: functions, }, }, @@ -300,14 +314,14 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { functions["Compress"] = common.PipelineFunction{ Parameters: map[string]string{Algorithm: CompressGZIP}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "Compress, SetOutputData", + ExecutionOrder: "Compress, SetResponseData", UseTargetTypeOfByteArray: true, Functions: functions, }, @@ -317,9 +331,9 @@ func TestUseTargetTypeOfByteArrayTrue(t *testing.T) { _, err := sdk.LoadConfigurablePipeline() require.NoError(t, err) - require.NotNil(t, sdk.TargetType) - assert.Equal(t, reflect.Ptr, reflect.TypeOf(sdk.TargetType).Kind()) - assert.Equal(t, reflect.TypeOf([]byte{}).Kind(), reflect.TypeOf(sdk.TargetType).Elem().Kind()) + require.NotNil(t, sdk.targetType) + assert.Equal(t, reflect.Ptr, reflect.TypeOf(sdk.targetType).Kind()) + assert.Equal(t, reflect.TypeOf([]byte{}).Kind(), reflect.TypeOf(sdk.targetType).Elem().Kind()) } func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { @@ -327,14 +341,14 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { functions["Compress"] = common.PipelineFunction{ Parameters: map[string]string{Algorithm: CompressGZIP}, } - functions["SetOutputData"] = common.PipelineFunction{} + functions["SetResponseData"] = common.PipelineFunction{} - sdk := AppFunctionsSDK{ - LoggingClient: lc, + sdk := Service{ + lc: lc, config: &common.ConfigurationStruct{ Writable: common.WritableInfo{ Pipeline: common.PipelineInfo{ - ExecutionOrder: "Compress, SetOutputData", + ExecutionOrder: "Compress, SetResponseData", UseTargetTypeOfByteArray: false, Functions: functions, }, @@ -344,13 +358,14 @@ func TestUseTargetTypeOfByteArrayFalse(t *testing.T) { _, err := sdk.LoadConfigurablePipeline() require.NoError(t, err) - assert.Nil(t, sdk.TargetType) + assert.Nil(t, sdk.targetType) } func TestSetServiceKey(t *testing.T) { - sdk := AppFunctionsSDK{ - LoggingClient: lc, - ServiceKey: "MyAppService", + sdk := Service{ + lc: lc, + serviceKey: "MyAppService", + profileSuffixPlaceholder: interfaces.ProfileSuffixPlaceholder, } tests := []struct { @@ -365,13 +380,13 @@ func TestSetServiceKey(t *testing.T) { }{ { name: "No profile", - originalServiceKey: "MyAppService" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService", }, { name: "Profile specified, no override", profile: "mqtt-export", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-mqtt-export", }, { @@ -379,14 +394,14 @@ func TestSetServiceKey(t *testing.T) { profile: "rules-engine", profileEnvVar: envProfile, profileEnvValue: "rules-engine-redis", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-rules-engine-redis", }, { name: "No profile specified with V2 override", profileEnvVar: envProfile, profileEnvValue: "http-export", - originalServiceKey: "MyAppService-" + ProfileSuffixPlaceholder, + originalServiceKey: "MyAppService-" + interfaces.ProfileSuffixPlaceholder, expectedServiceKey: "MyAppService-http-export", }, { @@ -446,13 +461,13 @@ func TestSetServiceKey(t *testing.T) { defer os.Clearenv() if len(test.serviceKeyCommandLineOverride) > 0 { - sdk.serviceKeyOverride = test.serviceKeyCommandLineOverride + sdk.commandLine.serviceKeyOverride = test.serviceKeyCommandLineOverride } - sdk.ServiceKey = test.originalServiceKey + sdk.serviceKey = test.originalServiceKey sdk.setServiceKey(test.profile) - assert.Equal(t, test.expectedServiceKey, sdk.ServiceKey) + assert.Equal(t, test.expectedServiceKey, sdk.serviceKey) }) } } @@ -460,16 +475,18 @@ func TestSetServiceKey(t *testing.T) { func TestMakeItStop(t *testing.T) { stopCalled := false - sdk := AppFunctionsSDK{ - stop: func() { - stopCalled = true + sdk := Service{ + ctx: contextGroup{ + stop: func() { + stopCalled = true + }, }, - LoggingClient: logger.NewMockClient(), + lc: logger.NewMockClient(), } sdk.MakeItStop() - require.True(t, stopCalled, "Cancel function set at sdk.stop should be called if set") + require.True(t, stopCalled, "Cancel function set at svc.stop should be called if set") - sdk.stop = nil + sdk.ctx.stop = nil sdk.MakeItStop() //should avoid nil pointer } diff --git a/internal/app/triggerfactory.go b/internal/app/triggerfactory.go new file mode 100644 index 000000000..85c0f2c22 --- /dev/null +++ b/internal/app/triggerfactory.go @@ -0,0 +1,121 @@ +// +// Copyright (c) 2020 Technocrats +// Copyright (c) 2021 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 app + +import ( + "errors" + "fmt" + "strings" + + "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) + +const ( + // Valid types of App Service triggers + TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" + TriggerTypeMQTT = "EXTERNAL-MQTT" + TriggerTypeHTTP = "HTTP" +) + +// RegisterCustomTriggerFactory allows users to register builders for custom trigger types +func (svc *Service) RegisterCustomTriggerFactory(name string, + factory func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error { + nu := strings.ToUpper(name) + + if nu == TriggerTypeMessageBus || + nu == TriggerTypeHTTP || + nu == TriggerTypeMQTT { + return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + } + + if svc.customTriggerFactories == nil { + svc.customTriggerFactories = make(map[string]func(sdk *Service) (interfaces.Trigger, error), 1) + } + + svc.customTriggerFactories[nu] = func(sdk *Service) (interfaces.Trigger, error) { + return factory(interfaces.TriggerConfig{ + Config: sdk.config.Trigger.EdgexMessageBus, + Logger: sdk.lc, + ContextBuilder: sdk.defaultTriggerContextBuilder, + MessageProcessor: sdk.defaultTriggerMessageProcessor, + }) + } + + return nil +} + +func (svc *Service) defaultTriggerMessageProcessor(appContext interfaces.AppFunctionContext, envelope types.MessageEnvelope) error { + context, ok := appContext.(*appfunction.Context) + if !ok { + return fmt.Errorf("App Context must be an istance of internal appfunction.Context. Use NewAppContext to create instance.") + } + + messageError := svc.runtime.ProcessMessage(context, envelope) + if messageError != nil { + // ProcessMessage logs the error, so no need to log it here. + return messageError.Err + } + + return nil +} + +func (svc *Service) defaultTriggerContextBuilder(env types.MessageEnvelope) interfaces.AppFunctionContext { + return appfunction.NewContext(env.CorrelationID, svc.dic, env.ContentType) +} + +func (svc *Service) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) interfaces.Trigger { + var t interfaces.Trigger + // Need to make dynamic, search for the trigger that is input + + switch triggerType := strings.ToUpper(configuration.Trigger.Type); triggerType { + case TriggerTypeHTTP: + svc.LoggingClient().Info("HTTP trigger selected") + t = http.NewTrigger(svc.dic, runtime, svc.webserver) + + case TriggerTypeMessageBus: + svc.LoggingClient().Info("EdgeX MessageBus trigger selected") + t = messagebus.NewTrigger(svc.dic, runtime) + + case TriggerTypeMQTT: + svc.LoggingClient().Info("External MQTT trigger selected") + t = mqtt.NewTrigger(svc.dic, runtime) + + default: + if factory, found := svc.customTriggerFactories[triggerType]; found { + var err error + t, err = factory(svc) + if err != nil { + svc.LoggingClient().Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) + return nil + } + } else { + svc.LoggingClient().Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type)) + } + } + + return t +} diff --git a/appsdk/triggerfactory_test.go b/internal/app/triggerfactory_test.go similarity index 81% rename from appsdk/triggerfactory_test.go rename to internal/app/triggerfactory_test.go index d862ab989..1624bb34a 100644 --- a/appsdk/triggerfactory_test.go +++ b/internal/app/triggerfactory_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +15,7 @@ // limitations under the License. // -package appsdk +package app import ( "context" @@ -23,10 +24,14 @@ import ( "sync" "testing" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/http" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/messagebus" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/trigger/mqtt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" @@ -39,7 +44,7 @@ import ( func TestRegisterCustomTriggerFactory_HTTP(t *testing.T) { name := strings.ToTitle(TriggerTypeHTTP) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -49,7 +54,7 @@ func TestRegisterCustomTriggerFactory_HTTP(t *testing.T) { func TestRegisterCustomTriggerFactory_EdgeXMessageBus(t *testing.T) { name := strings.ToTitle(TriggerTypeMessageBus) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -59,7 +64,7 @@ func TestRegisterCustomTriggerFactory_EdgeXMessageBus(t *testing.T) { func TestRegisterCustomTriggerFactory_MQTT(t *testing.T) { name := strings.ToTitle(TriggerTypeMQTT) - sdk := AppFunctionsSDK{} + sdk := Service{} err := sdk.RegisterCustomTriggerFactory(name, nil) require.Error(t, err, "should throw error") @@ -70,10 +75,11 @@ func TestRegisterCustomTrigger(t *testing.T) { name := "cUsToM tRiGgEr" trig := mockCustomTrigger{} - builder := func(c TriggerConfig) (Trigger, error) { + builder := func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &trig, nil } - sdk := AppFunctionsSDK{} + sdk := Service{config: &common.ConfigurationStruct{}} + err := sdk.RegisterCustomTriggerFactory(name, builder) require.Nil(t, err, "should not throw error") @@ -88,13 +94,13 @@ func TestRegisterCustomTrigger(t *testing.T) { } func TestSetupTrigger_HTTP(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeHTTP, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -104,13 +110,13 @@ func TestSetupTrigger_HTTP(t *testing.T) { } func TestSetupTrigger_EdgeXMessageBus(t *testing.T) { - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: TriggerTypeMessageBus, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -120,13 +126,22 @@ func TestSetupTrigger_EdgeXMessageBus(t *testing.T) { } func TestSetupTrigger_MQTT(t *testing.T) { - sdk := AppFunctionsSDK{ - config: &common.ConfigurationStruct{ - Trigger: common.TriggerInfo{ - Type: TriggerTypeMQTT, - }, + config := &common.ConfigurationStruct{ + Trigger: common.TriggerInfo{ + Type: TriggerTypeMQTT, }, - LoggingClient: logger.MockLogger{}, + } + + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return config + }, + }) + + sdk := Service{ + dic: dic, + config: config, + lc: lc, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) @@ -145,16 +160,16 @@ func (*mockCustomTrigger) Initialize(_ *sync.WaitGroup, _ context.Context, _ <-c func TestSetupTrigger_CustomType(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } - err := sdk.RegisterCustomTriggerFactory(triggerName, func(c TriggerConfig) (Trigger, error) { + err := sdk.RegisterCustomTriggerFactory(triggerName, func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &mockCustomTrigger{}, nil }) require.NoError(t, err) @@ -168,16 +183,16 @@ func TestSetupTrigger_CustomType(t *testing.T) { func TestSetupTrigger_CustomType_Error(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } - err := sdk.RegisterCustomTriggerFactory(triggerName, func(c TriggerConfig) (Trigger, error) { + err := sdk.RegisterCustomTriggerFactory(triggerName, func(c interfaces.TriggerConfig) (interfaces.Trigger, error) { return &mockCustomTrigger{}, errors.New("this should force returning nil even though we'll have a value") }) require.NoError(t, err) @@ -190,13 +205,13 @@ func TestSetupTrigger_CustomType_Error(t *testing.T) { func TestSetupTrigger_CustomType_NotFound(t *testing.T) { triggerName := uuid.New().String() - sdk := AppFunctionsSDK{ + sdk := Service{ config: &common.ConfigurationStruct{ Trigger: common.TriggerInfo{ Type: triggerName, }, }, - LoggingClient: logger.MockLogger{}, + lc: logger.MockLogger{}, } trigger := sdk.setupTrigger(sdk.config, sdk.runtime) diff --git a/internal/appfunction/context.go b/internal/appfunction/context.go new file mode 100644 index 000000000..901630db0 --- /dev/null +++ b/internal/appfunction/context.go @@ -0,0 +1,220 @@ +// +// Copyright (c) 2021 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 appfunction + +import ( + "context" + "fmt" + "time" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/handlers" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/models" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" + + "github.com/google/uuid" +) + +// NewContext creates, initializes and return a new Context with implements the interfaces.AppFunctionContext interface +func NewContext(correlationID string, dic *di.Container, inputContentType string) *Context { + return &Context{ + correlationID: correlationID, + dic: dic, + inputContentType: inputContentType, + } +} + +// Context contains the data functions that implement the interfaces.AppFunctionContext +type Context struct { + dic *di.Container + correlationID string + inputContentType string + responseData []byte + retryData []byte + responseContentType string +} + +// SetCorrelationID sets the correlationID. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) SetCorrelationID(id string) { + appContext.correlationID = id +} + +// CorrelationID returns context's the correlation ID +func (appContext *Context) CorrelationID() string { + return appContext.correlationID +} + +// SetInputContentType sets the inputContentType. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) SetInputContentType(contentType string) { + appContext.inputContentType = contentType +} + +// InputContentType returns the context's inputContentType +func (appContext *Context) InputContentType() string { + return appContext.inputContentType +} + +// SetResponseData provides a way to return the specified data as a response to the trigger that initiated +// the execution of the function pipeline. In the case of an HTTP Trigger, the data will be returned as the http response. +// In the case of a message bus trigger, the data will be published to the configured message bus publish topic. +func (appContext *Context) SetResponseData(output []byte) { + appContext.responseData = output +} + +// ResponseData returns the context's responseData. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) ResponseData() []byte { + return appContext.responseData +} + +// SetResponseContentType sets the context's responseContentType +func (appContext *Context) SetResponseContentType(contentType string) { + appContext.responseContentType = contentType +} + +// ResponseContentType returns the context's responseContentType +func (appContext *Context) ResponseContentType() string { + return appContext.responseContentType +} + +// SetRetryData sets the context's retryData to the specified payload to be stored for later retry +// when the pipeline function returns an error. +func (appContext *Context) SetRetryData(payload []byte) { + appContext.retryData = payload +} + +// RetryData returns the context's retryData. This function is not part of the AppFunctionContext interface, +// so it is internal SDK use only +func (appContext *Context) RetryData() []byte { + return appContext.retryData +} + +// PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. +// TODO: This function must be reworked for the new V2 Event Client +func (appContext *Context) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { + lc := appContext.LoggingClient() + lc.Debug("Pushing to CoreData") + + if appContext.EventClient() == nil { + return nil, fmt.Errorf("unable to Push To CoreData: '%s' is missing from Clients configuration", handlers.CoreDataClientName) + } + + now := time.Now().UnixNano() + val, err := util.CoerceType(value) + if err != nil { + return nil, err + } + + // Temporary use V1 Reading until V2 EventClient is available + // TODO: Change to use dtos.Reading + v1Reading := models.Reading{ + Value: string(val), + ValueType: v2.ValueTypeString, + Origin: now, + Device: deviceName, + Name: readingName, + } + + readings := make([]models.Reading, 0, 1) + readings = append(readings, v1Reading) + + // Temporary use V1 Event until V2 EventClient is available + // TODO: Change to use dtos.Event + v1Event := &models.Event{ + Device: deviceName, + Origin: now, + Readings: readings, + } + + correlation := uuid.New().String() + ctx := context.WithValue(context.Background(), clients.CorrelationHeader, correlation) + result, err := appContext.EventClient().Add(ctx, v1Event) // TODO: Update to use V2 EventClient + if err != nil { + return nil, err + } + v1Event.ID = result + + // TODO: Remove once V2 EventClient is available + v2Reading := dtos.BaseReading{ + Versionable: commonDTO.NewVersionable(), + Id: v1Reading.Id, + Created: v1Reading.Created, + Origin: v1Reading.Origin, + DeviceName: v1Reading.Device, + ResourceName: v1Reading.Name, + ProfileName: "", + ValueType: v1Reading.ValueType, + SimpleReading: dtos.SimpleReading{Value: v1Reading.Value}, + } + + // TODO: Remove once V2 EventClient is available + v2Event := dtos.Event{ + Versionable: commonDTO.NewVersionable(), + Id: result, + DeviceName: v1Event.Device, + Origin: v1Event.Origin, + Readings: []dtos.BaseReading{v2Reading}, + } + return &v2Event, nil +} + +// GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. +func (appContext *Context) GetSecret(path string, keys ...string) (map[string]string, error) { + secretProvider := bootstrapContainer.SecretProviderFrom(appContext.dic.Get) + return secretProvider.GetSecrets(path, keys...) +} + +// SecretsLastUpdated returns that timestamp for when the secrets in the SecretStore where last updated. +func (appContext *Context) SecretsLastUpdated() time.Time { + secretProvider := bootstrapContainer.SecretProviderFrom(appContext.dic.Get) + return secretProvider.SecretsLastUpdated() +} + +// LoggingClient returns the Logging client from the dependency injection container +func (appContext *Context) LoggingClient() logger.LoggingClient { + return bootstrapContainer.LoggingClientFrom(appContext.dic.Get) +} + +// EventClient returns the Event client, which may be nil, from the dependency injection container +func (appContext *Context) EventClient() coredata.EventClient { + return container.EventClientFrom(appContext.dic.Get) +} + +// CommandClient returns the Command client, which may be nil, from the dependency injection container +func (appContext *Context) CommandClient() command.CommandClient { + return container.CommandClientFrom(appContext.dic.Get) +} + +// NotificationsClient returns the Notifications client, which may be nil, from the dependency injection container +func (appContext *Context) NotificationsClient() notifications.NotificationsClient { + return container.NotificationsClientFrom(appContext.dic.Get) + +} diff --git a/internal/appfunction/context_test.go b/internal/appfunction/context_test.go new file mode 100644 index 000000000..b55f94f64 --- /dev/null +++ b/internal/appfunction/context_test.go @@ -0,0 +1,265 @@ +// +// Copyright (c) 2021 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 appfunction + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" +) + +var target *Context +var dic *di.Container + +func TestMain(m *testing.M) { + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.EventClientName: func(get di.Get) interface{} { + return coredata.NewEventClient(local.New(clients.ApiEventRoute)) + }, + container.NotificationsClientName: func(get di.Get) interface{} { + return notifications.NewNotificationsClient(local.New(clients.ApiNotificationRoute)) + }, + container.CommandClientName: func(get di.Get) interface{} { + return command.NewCommandClient(local.New(clients.ApiCommandRoute)) + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + + }, + }) + target = NewContext("", dic, "") + + os.Exit(m.Run()) +} + +func TestContext_PushToCoreData(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("newId")) + if r.Method != http.MethodPost { + t.Errorf("expected http method is POST, active http method is : %s", r.Method) + } + url := clients.ApiEventRoute + if r.URL.EscapedPath() != url { + t.Errorf("expected uri path is %s, actual uri path is %s", url, r.URL.EscapedPath()) + } + })) + + defer ts.Close() + + eventClient := coredata.NewEventClient(local.New(ts.URL + clients.ApiEventRoute)) + dic.Update(di.ServiceConstructorMap{ + container.EventClientName: func(get di.Get) interface{} { + return eventClient + }, + }) + + expectedEvent := &dtos.Event{ + Versionable: common.NewVersionable(), + DeviceName: "device-name", + Readings: []dtos.BaseReading{ + { + Versionable: common.NewVersionable(), + DeviceName: "device-name", + ResourceName: "device-resource", + ValueType: v2.ValueTypeString, + SimpleReading: dtos.SimpleReading{ + Value: "value", + }, + }, + }, + } + actualEvent, err := target.PushToCoreData("device-name", "device-resource", "value") + require.NoError(t, err) + + assert.NotNil(t, actualEvent) + assert.Equal(t, expectedEvent.ApiVersion, actualEvent.ApiVersion) + assert.Equal(t, expectedEvent.DeviceName, actualEvent.DeviceName) + assert.True(t, len(expectedEvent.Readings) == 1) + assert.Equal(t, expectedEvent.Readings[0].DeviceName, actualEvent.Readings[0].DeviceName) + assert.Equal(t, expectedEvent.Readings[0].ResourceName, actualEvent.Readings[0].ResourceName) + assert.Equal(t, expectedEvent.Readings[0].Value, actualEvent.Readings[0].Value) + assert.Equal(t, expectedEvent.Readings[0].ValueType, actualEvent.Readings[0].ValueType) + assert.Equal(t, expectedEvent.Readings[0].ApiVersion, actualEvent.Readings[0].ApiVersion) +} + +func TestContext_CommandClient(t *testing.T) { + actual := target.CommandClient() + assert.NotNil(t, actual) +} + +func TestContext_EventClient(t *testing.T) { + actual := target.EventClient() + assert.NotNil(t, actual) +} + +func TestContext_LoggingClient(t *testing.T) { + actual := target.LoggingClient() + assert.NotNil(t, actual) +} + +func TestContext_NotificationsClient(t *testing.T) { + actual := target.NotificationsClient() + assert.NotNil(t, actual) +} + +func TestContext_CorrelationID(t *testing.T) { + expected := "123-3456" + target.correlationID = expected + + actual := target.CorrelationID() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetCorrelationID(t *testing.T) { + expected := "567-098" + + target.SetCorrelationID(expected) + actual := target.correlationID + + assert.Equal(t, expected, actual) +} + +func TestContext_InputContentType(t *testing.T) { + expected := clients.ContentTypeXML + target.inputContentType = expected + + actual := target.InputContentType() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetInputContentType(t *testing.T) { + expected := clients.ContentTypeCBOR + + target.SetInputContentType(expected) + actual := target.inputContentType + + assert.Equal(t, expected, actual) +} + +func TestContext_ResponseContentType(t *testing.T) { + expected := clients.ContentTypeJSON + target.responseContentType = expected + + actual := target.ResponseContentType() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetResponseContentType(t *testing.T) { + expected := clients.ContentTypeText + + target.SetResponseContentType(expected) + actual := target.responseContentType + + assert.Equal(t, expected, actual) +} + +func TestContext_SetResponseData(t *testing.T) { + expected := []byte("response data") + + target.SetResponseData(expected) + actual := target.responseData + + assert.Equal(t, expected, actual) +} + +func TestContext_ResponseData(t *testing.T) { + expected := []byte("response data") + target.responseData = expected + + actual := target.ResponseData() + + assert.Equal(t, expected, actual) +} + +func TestContext_SetRetryData(t *testing.T) { + expected := []byte("retry data") + + target.SetRetryData(expected) + actual := target.retryData + + assert.Equal(t, expected, actual) +} + +func TestContext_RetryData(t *testing.T) { + expected := []byte("retry data") + target.retryData = expected + + actual := target.RetryData() + + assert.Equal(t, expected, actual) +} + +func TestContext_GetSecret(t *testing.T) { + // setup mock secret client + expected := map[string]string{ + "username": "TEST_USER", + "password": "TEST_PASS", + } + + mockSecretProvider := &mocks.SecretProvider{} + mockSecretProvider.On("GetSecrets", "mqtt").Return(expected, nil) + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + actual, err := target.GetSecret("mqtt") + require.NoError(t, err) + assert.Equal(t, expected, actual) +} + +func TestContext_SecretsLastUpdated(t *testing.T) { + expected := time.Now() + mockSecretProvider := &mocks.SecretProvider{} + mockSecretProvider.On("SecretsLastUpdated").Return(expected, nil) + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + actual := target.SecretsLastUpdated() + assert.Equal(t, expected, actual) +} diff --git a/internal/bootstrap/container/config.go b/internal/bootstrap/container/config.go index 6ba6dd311..55fc48ba7 100644 --- a/internal/bootstrap/container/config.go +++ b/internal/bootstrap/container/config.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package container import ( - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) // 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. +// ConfigurationFrom helper function queries the DIC and returns service's common.ConfigurationStruct implementation. func ConfigurationFrom(get di.Get) *common.ConfigurationStruct { return get(ConfigurationName).(*common.ConfigurationStruct) } diff --git a/internal/bootstrap/handlers/clients.go b/internal/bootstrap/handlers/clients.go index caab73d6b..96c64cd01 100644 --- a/internal/bootstrap/handlers/clients.go +++ b/internal/bootstrap/handlers/clients.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,7 +28,12 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" +) + +const ( + CoreCommandClientName = "Command" + CoreDataClientName = "CoreData" + NotificationsClientName = "Notifications" ) // Clients contains references to dependencies required by the Clients bootstrap implementation. @@ -42,9 +47,9 @@ func NewClients() *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, + _ context.Context, + _ *sync.WaitGroup, + _ startup.Timer, dic *di.Container) bool { config := container.ConfigurationFrom(dic.Get) @@ -56,22 +61,22 @@ func (_ *Clients) BootstrapHandler( // 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 { + if _, ok := config.Clients[CoreDataClientName]; ok { eventClient = coredata.NewEventClient( - local.New(config.Clients[common.CoreDataClientName].Url() + clients.ApiEventRoute)) + local.New(config.Clients[CoreDataClientName].Url() + clients.ApiEventRoute)) valueDescriptorClient = coredata.NewValueDescriptorClient( - local.New(config.Clients[common.CoreDataClientName].Url() + clients.ApiValueDescriptorRoute)) + local.New(config.Clients[CoreDataClientName].Url() + clients.ApiValueDescriptorRoute)) } - if _, ok := config.Clients[common.CoreCommandClientName]; ok { + if _, ok := config.Clients[CoreCommandClientName]; ok { commandClient = command.NewCommandClient( - local.New(config.Clients[common.CoreCommandClientName].Url() + clients.ApiDeviceRoute)) + local.New(config.Clients[CoreCommandClientName].Url() + clients.ApiDeviceRoute)) } - if _, ok := config.Clients[common.NotificationsClientName]; ok { + if _, ok := config.Clients[NotificationsClientName]; ok { notificationsClient = notifications.NewNotificationsClient( - local.New(config.Clients[common.NotificationsClientName].Url() + clients.ApiNotificationRoute)) + local.New(config.Clients[NotificationsClientName].Url() + clients.ApiNotificationRoute)) } // Note that all the clients are optional so some or all these clients may be nil diff --git a/internal/bootstrap/handlers/clients_test.go b/internal/bootstrap/handlers/clients_test.go index f04f5c2f9..bad02a3b3 100644 --- a/internal/bootstrap/handlers/clients_test.go +++ b/internal/bootstrap/handlers/clients_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,13 +24,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/startup" "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" ) @@ -102,15 +102,15 @@ func TestClientsBootstrapHandler(t *testing.T) { configuration.Clients = make(map[string]config.ClientInfo) if test.CoreDataClientInfo != nil { - configuration.Clients[common.CoreDataClientName] = coreDataClientInfo + configuration.Clients[CoreDataClientName] = coreDataClientInfo } if test.CommandClientInfo != nil { - configuration.Clients[common.CoreCommandClientName] = commandClientInfo + configuration.Clients[CoreCommandClientName] = commandClientInfo } if test.NotificationsClientInfo != nil { - configuration.Clients[common.NotificationsClientName] = notificationsClientInfo + configuration.Clients[NotificationsClientName] = notificationsClientInfo } dic.Update(di.ServiceConstructorMap{ diff --git a/internal/bootstrap/handlers/storeclient.go b/internal/bootstrap/handlers/storeclient.go index 512a83cc1..dab9614e8 100644 --- a/internal/bootstrap/handlers/storeclient.go +++ b/internal/bootstrap/handlers/storeclient.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ func NewDatabase() *Database { // BootstrapHandler creates the new interfaces.StoreClient use for database access by Store & Forward capability func (_ *Database) BootstrapHandler( - ctx context.Context, + _ context.Context, _ *sync.WaitGroup, startupTimer startup.Timer, dic *di.Container) bool { @@ -62,12 +62,12 @@ func (_ *Database) BootstrapHandler( return true } - logger := bootstrapContainer.LoggingClientFrom(dic.Get) + lc := bootstrapContainer.LoggingClientFrom(dic.Get) secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get) - storeClient, err := InitializeStoreClient(secretProvider, config, startupTimer, logger) + storeClient, err := InitializeStoreClient(secretProvider, config, startupTimer, lc) if err != nil { - logger.Error(err.Error()) + lc.Error(err.Error()) return false } diff --git a/internal/bootstrap/handlers/telemetry.go b/internal/bootstrap/handlers/telemetry.go index 9abc8983c..a9a63824d 100644 --- a/internal/bootstrap/handlers/telemetry.go +++ b/internal/bootstrap/handlers/telemetry.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ func NewTelemetry() *Telemetry { func (_ *Telemetry) BootstrapHandler( ctx context.Context, wg *sync.WaitGroup, - startupTimer startup.Timer, + _ startup.Timer, dic *di.Container) bool { logger := container.LoggingClientFrom(dic.Get) diff --git a/internal/bootstrap/handlers/version.go b/internal/bootstrap/handlers/version.go index cc7c59207..0cda1c5be 100644 --- a/internal/bootstrap/handlers/version.go +++ b/internal/bootstrap/handlers/version.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) const ( @@ -85,7 +84,7 @@ func (vv *VersionValidator) BootstrapHandler( return true } - url := config.Clients[common.CoreDataClientName].Url() + clients.ApiVersionRoute + url := config.Clients[CoreDataClientName].Url() + clients.ApiVersionRoute var data []byte var err error for startupTimer.HasNotElapsed() { diff --git a/internal/bootstrap/handlers/version_test.go b/internal/bootstrap/handlers/version_test.go index a1fe2dea5..ee3704865 100644 --- a/internal/bootstrap/handlers/version_test.go +++ b/internal/bootstrap/handlers/version_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ func TestValidateVersionMatch(t *testing.T) { startupTimer := startup.NewStartUpTimer("unit-test") clients := make(map[string]config.ClientInfo) - clients[common.CoreDataClientName] = config.ClientInfo{ + clients[CoreDataClientName] = config.ClientInfo{ Protocol: "http", Host: "localhost", Port: 0, // Will be replaced by local test webserver's port @@ -115,9 +115,9 @@ func TestValidateVersionMatch(t *testing.T) { testServerUrl, _ := url.Parse(testServer.URL) port, _ := strconv.Atoi(testServerUrl.Port()) - coreService := configuration.Clients[common.CoreDataClientName] + coreService := configuration.Clients[CoreDataClientName] coreService.Port = port - configuration.Clients[common.CoreDataClientName] = coreService + configuration.Clients[CoreDataClientName] = coreService validator := NewVersionValidator(test.skipVersionCheck, test.SdkVersion) result := validator.BootstrapHandler(context.Background(), &sync.WaitGroup{}, startupTimer, dic) diff --git a/internal/common/clients.go b/internal/common/clients.go deleted file mode 100644 index 64baa1902..000000000 --- a/internal/common/clients.go +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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 common - -import ( - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" -) - -const ( - CoreCommandClientName = "Command" - CoreDataClientName = "CoreData" - NotificationsClientName = "Notifications" -) - -type EdgeXClients struct { - LoggingClient logger.LoggingClient - EventClient coredata.EventClient - CommandClient command.CommandClient - ValueDescriptorClient coredata.ValueDescriptorClient - NotificationsClient notifications.NotificationsClient -} diff --git a/internal/constants.go b/internal/constants.go index 7e800dd9a..08b1ed9a3 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,7 +22,6 @@ import ( const ( ConfigRegistryStem = "edgex/appservices/1.0/" - DatabaseName = "application-service" CorrelationHeaderKey = "X-Correlation-ID" ApiTriggerRoute = contracts.ApiBase + "/trigger" @@ -30,7 +29,7 @@ const ( ) // SDKVersion indicates the version of the SDK - will be overwritten by build -var SDKVersion string = "0.0.0" +var SDKVersion = "0.0.0" // ApplicationVersion indicates the version of the application itself, not the SDK - will be overwritten by build -var ApplicationVersion string = "0.0.0" +var ApplicationVersion = "0.0.0" diff --git a/internal/controller/rest/controller.go b/internal/controller/rest/controller.go index 272180ec6..66ea1e2a5 100644 --- a/internal/controller/rest/controller.go +++ b/internal/controller/rest/controller.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,7 +22,11 @@ import ( "net/http" "strings" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/telemetry" @@ -46,16 +50,12 @@ type Controller struct { } // NewController creates and initializes an Controller -func NewController( - router *mux.Router, - lc logger.LoggingClient, - config *sdkCommon.ConfigurationStruct, - secretProvider interfaces.SecretProvider) *Controller { +func NewController(router *mux.Router, dic *di.Container) *Controller { return &Controller{ router: router, - secretProvider: secretProvider, - lc: lc, - config: config, + secretProvider: bootstrapContainer.SecretProviderFrom(dic.Get), + lc: bootstrapContainer.LoggingClientFrom(dic.Get), + config: container.ConfigurationFrom(dic.Get), } } diff --git a/internal/controller/rest/controller_test.go b/internal/controller/rest/controller_test.go index a867ac5a7..740341709 100644 --- a/internal/controller/rest/controller_test.go +++ b/internal/controller/rest/controller_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 202` 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,7 +27,11 @@ import ( "testing" "time" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" sdkCommon "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" @@ -45,9 +49,18 @@ import ( ) var expectedCorrelationId = uuid.New().String() +var dic *di.Container + +func TestMain(m *testing.M) { + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) +} func TestPingRequest(t *testing.T) { - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiPingRoute, target.Ping, nil) @@ -68,7 +81,7 @@ func TestVersionRequest(t *testing.T) { internal.ApplicationVersion = expectedAppVersion internal.SDKVersion = expectedSdkVersion - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiVersion, target.Version, nil) @@ -82,7 +95,7 @@ func TestVersionRequest(t *testing.T) { } func TestMetricsRequest(t *testing.T) { - target := NewController(nil, logger.NewMockClient(), nil, nil) + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiMetricsRoute, target.Metrics, nil) @@ -112,7 +125,13 @@ func TestConfigRequest(t *testing.T) { }, } - target := NewController(nil, logger.NewMockClient(), &expectedConfig, nil) + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &expectedConfig + }, + }) + + target := NewController(nil, dic) recorder := doRequest(t, http.MethodGet, contracts.ApiConfigRoute, target.Config, nil) @@ -136,15 +155,18 @@ func TestConfigRequest(t *testing.T) { func TestAddSecretRequest(t *testing.T) { expectedRequestId := "82eb2e26-0f24-48aa-ae4c-de9dac3fb9bc" - config := &sdkCommon.ConfigurationStruct{} - lc := logger.NewMockClient() + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &sdkCommon.ConfigurationStruct{} + }, + }) mockProvider := &mocks.SecretProvider{} mockProvider.On("StoreSecrets", "/mqtt", map[string]string{"password": "password", "username": "username"}).Return(nil) mockProvider.On("StoreSecrets", "/no", map[string]string{"password": "password", "username": "username"}).Return(errors.New("Invalid w/o Vault")) - target := NewController(nil, lc, config, mockProvider) + target := NewController(nil, dic) assert.NotNil(t, target) validRequest := common.SecretRequest{ diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 8f874bd9b..b749a2935 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,13 +26,13 @@ import ( "strings" "sync" - edgexErrors "github.com/edgexfoundry/go-mod-core-contracts/v2/errors" - "github.com/google/uuid" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + edgexErrors "github.com/edgexfoundry/go-mod-core-contracts/v2/errors" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" @@ -40,21 +40,18 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos/requests" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - dbInterfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" - "github.com/fxamacker/cbor/v2" + "github.com/google/uuid" ) // GolangRuntime represents the golang runtime environment type GolangRuntime struct { - TargetType interface{} - ServiceKey string - transforms []appcontext.AppFunction - isBusyCopying sync.Mutex - storeForward storeForwardInfo - secretProvider interfaces.SecretProvider + TargetType interface{} + ServiceKey string + transforms []interfaces.AppFunction + isBusyCopying sync.Mutex + storeForward storeForwardInfo + dic *di.Container } type MessageError struct { @@ -63,14 +60,14 @@ type MessageError struct { } // Initialize sets the internal reference to the StoreClient for use when Store and Forward is enabled -func (gr *GolangRuntime) Initialize(storeClient dbInterfaces.StoreClient, secretProvider interfaces.SecretProvider) { - gr.storeForward.storeClient = storeClient +func (gr *GolangRuntime) Initialize(dic *di.Container) { + gr.dic = dic gr.storeForward.runtime = gr - gr.secretProvider = secretProvider + gr.storeForward.dic = dic } // SetTransforms is thread safe to set transforms -func (gr *GolangRuntime) SetTransforms(transforms []appcontext.AppFunction) { +func (gr *GolangRuntime) SetTransforms(transforms []interfaces.AppFunction) { gr.isBusyCopying.Lock() gr.transforms = transforms gr.storeForward.pipelineHash = gr.storeForward.calculatePipelineHash() // Only need to calculate hash when the pipeline changes. @@ -78,8 +75,8 @@ func (gr *GolangRuntime) SetTransforms(transforms []appcontext.AppFunction) { } // ProcessMessage sends the contents of the message thru the functions pipeline -func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelope types.MessageEnvelope) *MessageError { - lc := edgexcontext.LoggingClient +func (gr *GolangRuntime) ProcessMessage(appContext *appfunction.Context, envelope types.MessageEnvelope) *MessageError { + lc := appContext.LoggingClient() if len(gr.transforms) == 0 { err := errors.New("No transforms configured. Please check log for errors loading pipeline") @@ -145,7 +142,7 @@ func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelo } } - edgexcontext.CorrelationID = envelope.CorrelationID + appContext.SetCorrelationID(envelope.CorrelationID) // All functions expect an object, not a pointer to an object, so must use reflection to // dereference to pointer to the object @@ -153,42 +150,46 @@ func (gr *GolangRuntime) ProcessMessage(edgexcontext *appcontext.Context, envelo // Make copy of transform functions to avoid disruption of pipeline when updating the pipeline from registry gr.isBusyCopying.Lock() - transforms := make([]appcontext.AppFunction, len(gr.transforms)) + transforms := make([]interfaces.AppFunction, len(gr.transforms)) copy(transforms, gr.transforms) gr.isBusyCopying.Unlock() - return gr.ExecutePipeline(target, envelope.ContentType, edgexcontext, transforms, 0, false) + return gr.ExecutePipeline(target, envelope.ContentType, appContext, transforms, 0, false) } -func (gr *GolangRuntime) ExecutePipeline(target interface{}, contentType string, edgexcontext *appcontext.Context, - transforms []appcontext.AppFunction, startPosition int, isRetry bool) *MessageError { +func (gr *GolangRuntime) ExecutePipeline( + target interface{}, + contentType string, + appContext *appfunction.Context, + transforms []interfaces.AppFunction, + startPosition int, + isRetry bool) *MessageError { var result interface{} var continuePipeline = true - edgexcontext.SecretProvider = gr.secretProvider - for functionIndex, trxFunc := range transforms { if functionIndex < startPosition { continue } - edgexcontext.RetryData = nil + appContext.SetRetryData(nil) if result == nil { - continuePipeline, result = trxFunc(edgexcontext, target, contentType) + appContext.SetInputContentType(contentType) + continuePipeline, result = trxFunc(appContext, target) } else { - continuePipeline, result = trxFunc(edgexcontext, result) + continuePipeline, result = trxFunc(appContext, result) } if continuePipeline != true { if result != nil { if err, ok := result.(error); ok { - edgexcontext.LoggingClient.Error( + appContext.LoggingClient().Error( fmt.Sprintf("Pipeline function #%d resulted in error", functionIndex), - "error", err.Error(), clients.CorrelationHeader, edgexcontext.CorrelationID) - if edgexcontext.RetryData != nil && !isRetry { - gr.storeForward.storeForLaterRetry(edgexcontext.RetryData, edgexcontext, functionIndex) + "error", err.Error(), clients.CorrelationHeader, appContext.CorrelationID) + if appContext.RetryData() != nil && !isRetry { + gr.storeForward.storeForLaterRetry(appContext.RetryData(), appContext, functionIndex) } return &MessageError{Err: err, ErrorCode: http.StatusUnprocessableEntity} @@ -206,11 +207,9 @@ func (gr *GolangRuntime) StartStoreAndForward( appCtx context.Context, enabledWg *sync.WaitGroup, enabledCtx context.Context, - serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { + serviceKey string) { - gr.storeForward.startStoreAndForwardRetryLoop(appWg, appCtx, enabledWg, enabledCtx, serviceKey, config, edgeXClients) + gr.storeForward.startStoreAndForwardRetryLoop(appWg, appCtx, enabledWg, enabledCtx, serviceKey) } func (gr *GolangRuntime) processEventPayload(envelope types.MessageEnvelope, lc logger.LoggingClient) (*dtos.Event, error) { diff --git a/internal/runtime/runtime_test.go b/internal/runtime/runtime_test.go index 8a8f8d18e..ba1fc3374 100644 --- a/internal/runtime/runtime_test.go +++ b/internal/runtime/runtime_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,13 +21,15 @@ import ( "net/http" "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" @@ -39,8 +41,6 @@ import ( "github.com/stretchr/testify/require" ) -var lc logger.LoggingClient - const ( serviceKey = "AppService-UnitTest" ) @@ -50,7 +50,7 @@ var testV2Event = testAddEventRequest.Event func createAddEventRequest() requests.AddEventRequest { event := dtos.NewEvent("Thermostat", "FamilyRoomThermostat", "Temperature") - event.AddSimpleReading("Temperature", v2.ValueTypeInt64, int64(72)) + _ = event.AddSimpleReading("Temperature", v2.ValueTypeInt64, int64(72)) request := requests.NewAddEventRequest(event) return request } @@ -74,11 +74,7 @@ var testV1Event = models.Event{ Tags: nil, } -func init() { - lc = logger.NewMockClient() -} - -func TestProcessMessageBasRequest(t *testing.T) { +func TestProcessMessageBusRequest(t *testing.T) { expected := http.StatusBadRequest badRequest := testAddEventRequest @@ -92,17 +88,15 @@ func TestProcessMessageBasRequest(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") - dummyTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + dummyTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{dummyTransform}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{dummyTransform}) result := runtime.ProcessMessage(context, envelope) require.NotNil(t, result) assert.Equal(t, expected, result.ErrorCode) @@ -118,12 +112,10 @@ func TestProcessMessageNoTransforms(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") runtime := GolangRuntime{} - runtime.Initialize(nil, nil) + runtime.Initialize(nil) result := runtime.ProcessMessage(context, envelope) require.NotNil(t, result) @@ -139,13 +131,12 @@ func TestProcessMessageOneCustomTransform(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + transform1WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") - if result, ok := params[0].(*dtos.Event); ok { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + require.NotNil(t, data, "should have been passed the first event from CoreData") + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") require.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event") } @@ -153,8 +144,8 @@ func TestProcessMessageOneCustomTransform(t *testing.T) { return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) require.True(t, transform1WasCalled, "transform1 should have been called") @@ -169,32 +160,30 @@ func TestProcessMessageTwoCustomTransforms(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") transform1WasCalled := false transform2WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") - if result, ok := params[0].(dtos.Event); ok { + require.NotNil(t, data, "should have been passed the first event from CoreData") + if result, ok := data.(dtos.Event); ok { require.True(t, ok, "Should have received Event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected Event") } return true, "Transform1Result" } - transform2 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform2 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform2WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1, transform2}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1, transform2}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) @@ -211,37 +200,36 @@ func TestProcessMessageThreeCustomTransformsOneFail(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + transform1WasCalled := false transform2WasCalled := false transform3WasCalled := false - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.True(t, len(params) > 0, "should have been passed the first event from CoreData") + require.NotNil(t, data, "should have been passed the first event from CoreData") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") require.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event") } return false, "Transform1Result" } - transform2 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform2 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform2WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } - transform3 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform3 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform3WasCalled = true - require.Equal(t, "Transform1Result", params[0], "Did not receive result from previous transform") + require.Equal(t, "Transform1Result", data, "Did not receive result from previous transform") return true, "Hello" } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1, transform2, transform3}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1, transform2, transform3}) result := runtime.ProcessMessage(context, envelope) require.Nil(t, result) @@ -265,14 +253,13 @@ func TestProcessMessageTransformError(t *testing.T) { Payload: payload, ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testId", dic, "") + // Let the Runtime know we are sending a RegistryInfo so it passes it to the first function runtime := GolangRuntime{TargetType: &config.RegistryInfo{}} - runtime.Initialize(nil, nil) + runtime.Initialize(nil) // FilterByDeviceName with return an error if it doesn't receive and Event - runtime.SetTransforms([]appcontext.AppFunction{transforms.NewFilterFor([]string{"SomeDevice"}).FilterByDeviceName}) + runtime.SetTransforms([]interfaces.AppFunction{transforms.NewFilterFor([]string{"SomeDevice"}).FilterByDeviceName}) err := runtime.ProcessMessage(context, envelope) require.NotNil(t, err, "Expected an error") @@ -295,17 +282,14 @@ func TestProcessMessageJSON(t *testing.T) { ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext("testing", dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -315,8 +299,8 @@ func TestProcessMessageJSON(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nilf(t, result, "result should be null. Got %v", result) @@ -337,17 +321,14 @@ func TestProcessMessageCBOR(t *testing.T) { ContentType: clients.ContentTypeCBOR, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext("testing", dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -357,8 +338,8 @@ func TestProcessMessageCBOR(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nil(t, result, "result should be null") @@ -369,7 +350,7 @@ type CustomType struct { ID string `json:"id"` } -// Must implement the Marshaller interface so SetOutputData will marshal it to JSON +// Must implement the Marshaller interface so SetResponseData will marshal it to JSON func (custom CustomType) MarshalJSON() ([]byte, error) { test := struct { ID string `json:"id"` @@ -424,13 +405,11 @@ func TestProcessMessageTargetType(t *testing.T) { ContentType: currentTest.ContentType, } - context := &appcontext.Context{ - LoggingClient: lc, - } + context := appfunction.NewContext("testing", dic, "") runtime := GolangRuntime{TargetType: currentTest.TargetType} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transforms.NewOutputData().SetOutputData}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transforms.NewResponseData().SetResponseData}) err := runtime.ProcessMessage(context, envelope) if currentTest.ErrorExpected { @@ -440,49 +419,36 @@ func TestProcessMessageTargetType(t *testing.T) { assert.Nil(t, err, fmt.Sprintf("unexpected error for test '%s'", currentTest.Name)) } - // OutputData will be nil if an error occurred in the pipeline processing the data - assert.Equal(t, currentTest.ExpectedOutputData, context.OutputData, fmt.Sprintf("'%s' test failed", currentTest.Name)) + // ResponseData will be nil if an error occurred in the pipeline processing the data + assert.Equal(t, currentTest.ExpectedOutputData, context.ResponseData(), fmt.Sprintf("'%s' test failed", currentTest.Name)) }) } } func TestExecutePipelinePersist(t *testing.T) { expectedItemCount := 1 - configuration := common.ConfigurationStruct{ - Writable: common.WritableInfo{ - LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{ - Enabled: true, - MaxRetryCount: 10}, - }, - } - - ctx := appcontext.Context{ - Configuration: &configuration, - LoggingClient: lc, - CorrelationID: "CorrelationID", - } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + context := appfunction.NewContext("testing", dic, "") + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } runtime := GolangRuntime{ServiceKey: serviceKey} - runtime.Initialize(creatMockStoreClient(), nil) + runtime.Initialize(updateDicWithMockStoreClient()) httpPost := transforms.NewHTTPSender("http://nowhere", "", true).HTTPPost - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, httpPost}) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, httpPost}) payload := []byte("My Payload") // Target of this test - actual := runtime.ExecutePipeline(payload, "", &ctx, runtime.transforms, 0, false) + actual := runtime.ExecutePipeline(payload, "", context, runtime.transforms, 0, false) require.NotNil(t, actual) require.Error(t, actual.Err, "Error expected from export function") storedObjects := mockRetrieveObjects(serviceKey) require.Equal(t, expectedItemCount, len(storedObjects), "unexpected item count") assert.Equal(t, serviceKey, storedObjects[0].AppServiceKey, "AppServiceKey not as expected") - assert.Equal(t, ctx.CorrelationID, storedObjects[0].CorrelationID, "CorrelationID not as expected") + assert.Equal(t, context.CorrelationID(), storedObjects[0].CorrelationID, "CorrelationID not as expected") } // TODO: Remove once switch completely to V2 Event DTOs @@ -498,17 +464,14 @@ func TestProcessMessageJSONWithV1Event(t *testing.T) { ContentType: clients.ContentTypeJSON, } - context := &appcontext.Context{ - LoggingClient: lc, - CorrelationID: expectedCorrelationID, - } + context := appfunction.NewContext(expectedCorrelationID, dic, "") - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { transform1WasCalled = true - require.Equal(t, expectedCorrelationID, edgexcontext.CorrelationID, "Context doesn't contain expected CorrelationID") + require.Equal(t, expectedCorrelationID, appContext.CorrelationID(), "Context doesn't contain expected CorrelationID") - if result, ok := params[0].(*dtos.Event); ok { + if result, ok := data.(*dtos.Event); ok { require.True(t, ok, "Should have received EdgeX event") assert.Equal(t, testV2Event.DeviceName, result.DeviceName, "Did not receive expected EdgeX event, wrong device") assert.Equal(t, testV2Event.Id, result.Id, "Did not receive expected EdgeX event, wrong ID") @@ -518,8 +481,8 @@ func TestProcessMessageJSONWithV1Event(t *testing.T) { } runtime := GolangRuntime{} - runtime.Initialize(nil, nil) - runtime.SetTransforms([]appcontext.AppFunction{transform1}) + runtime.Initialize(nil) + runtime.SetTransforms([]interfaces.AppFunction{transform1}) result := runtime.ProcessMessage(context, envelope) assert.Nil(t, result, "result should be null") @@ -573,7 +536,7 @@ func TestGolangRuntime_processEventPayload(t *testing.T) { envelope.Payload = testCase.Payload envelope.ContentType = testCase.ContentType - actual, err := target.processEventPayload(envelope, lc) + actual, err := target.processEventPayload(envelope, logger.NewMockClient()) if testCase.ExpectError { require.Error(t, err) return @@ -611,7 +574,7 @@ func TestGolangRuntime_unmarshalV1EventToV2Event(t *testing.T) { envelope.Payload = testCase.Payload envelope.ContentType = testCase.ContentType - actual, err := target.unmarshalV1EventToV2Event(envelope, lc) + actual, err := target.unmarshalV1EventToV2Event(envelope, logger.NewMockClient()) require.NoError(t, err) require.Equal(t, expectedEvent, *actual) }) diff --git a/internal/runtime/storeforward.go b/internal/runtime/storeforward.go index 352af89c2..39e54b2e8 100644 --- a/internal/runtime/storeforward.go +++ b/internal/runtime/storeforward.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,20 +24,23 @@ import ( "sync" "time" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/contracts" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" ) const ( - defaultMinRetryInterval = time.Duration(1 * time.Second) + defaultMinRetryInterval = 1 * time.Second ) type storeForwardInfo struct { runtime *GolangRuntime - storeClient interfaces.StoreClient + dic *di.Container pipelineHash string } @@ -46,37 +49,38 @@ func (sf *storeForwardInfo) startStoreAndForwardRetryLoop( appCtx context.Context, enabledWg *sync.WaitGroup, enabledCtx context.Context, - serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { + serviceKey string) { appWg.Add(1) enabledWg.Add(1) + config := container.ConfigurationFrom(sf.dic.Get) + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + go func() { defer appWg.Done() defer enabledWg.Done() retryInterval, err := time.ParseDuration(config.Writable.StoreAndForward.RetryInterval) if err != nil { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward RetryInterval failed to parse, defaulting to %s", defaultMinRetryInterval.String())) retryInterval = defaultMinRetryInterval } else if retryInterval < defaultMinRetryInterval { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward RetryInterval value %s is less than the allowed minimum value, defaulting to %s", retryInterval.String(), defaultMinRetryInterval.String())) retryInterval = defaultMinRetryInterval } if config.Writable.StoreAndForward.MaxRetryCount < 0 { - edgeXClients.LoggingClient.Warn( + lc.Warn( fmt.Sprintf("StoreAndForward MaxRetryCount can not be less than 0, defaulting to 1")) config.Writable.StoreAndForward.MaxRetryCount = 1 } - edgeXClients.LoggingClient.Info( + lc.Info( fmt.Sprintf("Starting StoreAndForward Retry Loop with %s RetryInterval and %d max retries", retryInterval.String(), config.Writable.StoreAndForward.MaxRetryCount)) @@ -93,61 +97,66 @@ func (sf *storeForwardInfo) startStoreAndForwardRetryLoop( break exit case <-time.After(retryInterval): - sf.retryStoredData(serviceKey, config, edgeXClients) + sf.retryStoredData(serviceKey) } } - edgeXClients.LoggingClient.Info("Exiting StoreAndForward Retry Loop") + lc.Info("Exiting StoreAndForward Retry Loop") }() } -func (sf *storeForwardInfo) storeForLaterRetry(payload []byte, - edgexcontext *appcontext.Context, +func (sf *storeForwardInfo) storeForLaterRetry( + payload []byte, + appContext interfaces.AppFunctionContext, pipelinePosition int) { item := contracts.NewStoredObject(sf.runtime.ServiceKey, payload, pipelinePosition, sf.pipelineHash) - item.CorrelationID = edgexcontext.CorrelationID + item.CorrelationID = appContext.CorrelationID() - edgexcontext.LoggingClient.Trace("Storing data for later retry", - clients.CorrelationHeader, edgexcontext.CorrelationID) + appContext.LoggingClient().Trace("Storing data for later retry", + clients.CorrelationHeader, appContext.CorrelationID) - if !edgexcontext.Configuration.Writable.StoreAndForward.Enabled { - edgexcontext.LoggingClient.Error( + config := container.ConfigurationFrom(sf.dic.Get) + if !config.Writable.StoreAndForward.Enabled { + appContext.LoggingClient().Error( "Failed to store item for later retry", "error", "StoreAndForward not enabled", clients.CorrelationHeader, item.CorrelationID) return } - if _, err := sf.storeClient.Store(item); err != nil { - edgexcontext.LoggingClient.Error("Failed to store item for later retry", + storeClient := container.StoreClientFrom(sf.dic.Get) + + if _, err := storeClient.Store(item); err != nil { + appContext.LoggingClient().Error("Failed to store item for later retry", "error", err, clients.CorrelationHeader, item.CorrelationID) } } -func (sf *storeForwardInfo) retryStoredData(serviceKey string, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) { +func (sf *storeForwardInfo) retryStoredData(serviceKey string) { - items, err := sf.storeClient.RetrieveFromStore(serviceKey) + storeClient := container.StoreClientFrom(sf.dic.Get) + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + + items, err := storeClient.RetrieveFromStore(serviceKey) if err != nil { - edgeXClients.LoggingClient.Error("Unable to load store and forward items from DB", "error", err) + lc.Error("Unable to load store and forward items from DB", "error", err) return } - edgeXClients.LoggingClient.Debug(fmt.Sprintf(" %d stored data items found for retrying", len(items))) + lc.Debug(fmt.Sprintf(" %d stored data items found for retrying", len(items))) if len(items) > 0 { - itemsToRemove, itemsToUpdate := sf.processRetryItems(items, config, edgeXClients) + itemsToRemove, itemsToUpdate := sf.processRetryItems(items) - edgeXClients.LoggingClient.Debug( + lc.Debug( fmt.Sprintf(" %d stored data items will be removed post retry", len(itemsToRemove))) - edgeXClients.LoggingClient.Debug( + lc.Debug( fmt.Sprintf(" %d stored data items will be update post retry", len(itemsToUpdate))) for _, item := range itemsToRemove { - if err := sf.storeClient.RemoveFromStore(item); err != nil { - edgeXClients.LoggingClient.Error( + if err := storeClient.RemoveFromStore(item); err != nil { + lc.Error( "Unable to remove stored data item from DB", "error", err, "objectID", item.ID, @@ -156,8 +165,8 @@ func (sf *storeForwardInfo) retryStoredData(serviceKey string, } for _, item := range itemsToUpdate { - if err := sf.storeClient.Update(item); err != nil { - edgeXClients.LoggingClient.Error("Unable to update stored data item in DB", + if err := storeClient.Update(item); err != nil { + lc.Error("Unable to update stored data item in DB", "error", err, "objectID", item.ID, clients.CorrelationHeader, item.CorrelationID) @@ -166,20 +175,20 @@ func (sf *storeForwardInfo) retryStoredData(serviceKey string, } } -func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, - config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) ([]contracts.StoredObject, []contracts.StoredObject) { +func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject) ([]contracts.StoredObject, []contracts.StoredObject) { + lc := bootstrapContainer.LoggingClientFrom(sf.dic.Get) + config := container.ConfigurationFrom(sf.dic.Get) var itemsToRemove []contracts.StoredObject var itemsToUpdate []contracts.StoredObject for _, item := range items { if item.Version == sf.calculatePipelineHash() { - if !sf.retryExportFunction(item, config, edgeXClients) { + if !sf.retryExportFunction(item) { item.RetryCount++ if config.Writable.StoreAndForward.MaxRetryCount == 0 || item.RetryCount < config.Writable.StoreAndForward.MaxRetryCount { - edgeXClients.LoggingClient.Trace("Export retry failed. Incrementing retry count", + lc.Trace("Export retry failed. Incrementing retry count", "retries", item.RetryCount, clients.CorrelationHeader, @@ -188,20 +197,20 @@ func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, continue } - edgeXClients.LoggingClient.Trace( + lc.Trace( "Max retries exceeded. Removing item from DB", "retries", item.RetryCount, clients.CorrelationHeader, item.CorrelationID) // Note that item will be removed for DB below. } else { - edgeXClients.LoggingClient.Trace( + lc.Trace( "Export retry successful. Removing item from DB", clients.CorrelationHeader, item.CorrelationID) } } else { - edgeXClients.LoggingClient.Error( + lc.Error( "Stored data item's Function Pipeline Version doesn't match current Function Pipeline Version. Removing item from DB", clients.CorrelationHeader, item.CorrelationID) @@ -218,24 +227,15 @@ func (sf *storeForwardInfo) processRetryItems(items []contracts.StoredObject, return itemsToRemove, itemsToUpdate } -func (sf *storeForwardInfo) retryExportFunction(item contracts.StoredObject, config *common.ConfigurationStruct, - edgeXClients common.EdgeXClients) bool { - edgexContext := &appcontext.Context{ - CorrelationID: item.CorrelationID, - Configuration: config, - LoggingClient: edgeXClients.LoggingClient, - EventClient: edgeXClients.EventClient, - ValueDescriptorClient: edgeXClients.ValueDescriptorClient, - CommandClient: edgeXClients.CommandClient, - NotificationsClient: edgeXClients.NotificationsClient, - } +func (sf *storeForwardInfo) retryExportFunction(item contracts.StoredObject) bool { + appContext := appfunction.NewContext(item.CorrelationID, sf.dic, "") - edgexContext.LoggingClient.Trace("Retrying stored data", clients.CorrelationHeader, edgexContext.CorrelationID) + appContext.LoggingClient().Trace("Retrying stored data", clients.CorrelationHeader, appContext.CorrelationID) return sf.runtime.ExecutePipeline( item.Payload, "", - edgexContext, + appContext, sf.runtime.transforms, item.PipelinePosition, true) == nil diff --git a/internal/runtime/storeforward_test.go b/internal/runtime/storeforward_test.go index 0b67ea586..b2f7e0573 100644 --- a/internal/runtime/storeforward_test.go +++ b/internal/runtime/storeforward_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,42 +18,61 @@ package runtime import ( "errors" + "os" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/stretchr/testify/require" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/contracts" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces/mocks" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/transforms" ) -func TestProcessRetryItems(t *testing.T) { - - targetTransformWasCalled := false - expectedPayload := "This is a sample payload" +var dic *di.Container +func TestMain(m *testing.M) { config := common.ConfigurationStruct{ Writable: common.WritableInfo{ LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{MaxRetryCount: 10}, + StoreAndForward: common.StoreAndForwardInfo{Enabled: true, MaxRetryCount: 10}, }, } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) + + os.Exit(m.Run()) +} + +func TestProcessRetryItems(t *testing.T) { + + targetTransformWasCalled := false + expectedPayload := "This is a sample payload" + + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } - successTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + successTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { targetTransformWasCalled = true - actualPayload, ok := params[0].([]byte) + actualPayload, ok := data.([]byte) require.True(t, ok, "Expected []byte payload") require.Equal(t, expectedPayload, string(actualPayload)) @@ -61,16 +80,15 @@ func TestProcessRetryItems(t *testing.T) { return false, nil } - failureTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + failureTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { targetTransformWasCalled = true return false, errors.New("I failed") } - runtime := GolangRuntime{} tests := []struct { Name string - TargetTransform appcontext.AppFunction + TargetTransform interfaces.AppFunction TargetTransformWasCalled bool ExpectedPayload string RetryCount int @@ -88,8 +106,8 @@ func TestProcessRetryItems(t *testing.T) { t.Run(test.Name, func(t *testing.T) { targetTransformWasCalled = false - runtime.Initialize(creatMockStoreClient(), nil) - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, transformPassthru, test.TargetTransform}) + runtime.Initialize(dic) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, transformPassthru, test.TargetTransform}) version := runtime.storeForward.pipelineHash if test.BadVersion { @@ -98,7 +116,7 @@ func TestProcessRetryItems(t *testing.T) { storedObject := contracts.NewStoredObject("dummy", []byte(test.ExpectedPayload), 2, version) storedObject.RetryCount = test.RetryCount - removes, updates := runtime.storeForward.processRetryItems([]contracts.StoredObject{storedObject}, &config, common.EdgeXClients{LoggingClient: lc}) + removes, updates := runtime.storeForward.processRetryItems([]contracts.StoredObject{storedObject}) assert.Equal(t, test.TargetTransformWasCalled, targetTransformWasCalled, "Target transform not called") if test.RetryCount != test.ExpectedRetryCount { if assert.True(t, len(updates) > 0, "Remove count not as expected") { @@ -113,23 +131,18 @@ func TestProcessRetryItems(t *testing.T) { func TestDoStoreAndForwardRetry(t *testing.T) { serviceKey := "AppService-UnitTest" payload := []byte("My Payload") - config := common.ConfigurationStruct{ - Writable: common.WritableInfo{ - LogLevel: "DEBUG", - StoreAndForward: common.StoreAndForwardInfo{MaxRetryCount: 10}}, - } httpPost := transforms.NewHTTPSender("http://nowhere", "", true).HTTPPost - successTransform := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { + successTransform := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { return false, nil } - transformPassthru := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return true, params[0] + transformPassthru := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return true, data } tests := []struct { Name string - TargetTransform appcontext.AppFunction + TargetTransform interfaces.AppFunction RetryCount int ExpectedRetryCount int ExpectedObjectCount int @@ -142,8 +155,8 @@ func TestDoStoreAndForwardRetry(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { runtime := GolangRuntime{ServiceKey: serviceKey} - runtime.Initialize(creatMockStoreClient(), nil) - runtime.SetTransforms([]appcontext.AppFunction{transformPassthru, test.TargetTransform}) + runtime.Initialize(updateDicWithMockStoreClient()) + runtime.SetTransforms([]interfaces.AppFunction{transformPassthru, test.TargetTransform}) object := contracts.NewStoredObject(serviceKey, payload, 1, runtime.storeForward.calculatePipelineHash()) object.CorrelationID = "CorrelationID" @@ -152,7 +165,7 @@ func TestDoStoreAndForwardRetry(t *testing.T) { _, _ = mockStoreObject(object) // Target of this test - runtime.storeForward.retryStoredData(serviceKey, &config, common.EdgeXClients{LoggingClient: lc}) + runtime.storeForward.retryStoredData(serviceKey) objects := mockRetrieveObjects(serviceKey) if assert.Equal(t, test.ExpectedObjectCount, len(objects)) && test.ExpectedObjectCount > 0 { @@ -166,7 +179,7 @@ func TestDoStoreAndForwardRetry(t *testing.T) { var mockObjectStore map[string]contracts.StoredObject -func creatMockStoreClient() interfaces.StoreClient { +func updateDicWithMockStoreClient() *di.Container { mockObjectStore = make(map[string]contracts.StoredObject) storeClient := &mocks.StoreClient{} storeClient.Mock.On("Store", mock.Anything).Return(mockStoreObject) @@ -174,7 +187,13 @@ func creatMockStoreClient() interfaces.StoreClient { storeClient.Mock.On("Update", mock.Anything).Return(mockUpdateObject) storeClient.Mock.On("RetrieveFromStore", mock.Anything).Return(mockRetrieveObjects, nil) - return storeClient + dic.Update(di.ServiceConstructorMap{ + container.StoreClientName: func(get di.Get) interface{} { + return storeClient + }, + }) + + return dic } func mockStoreObject(object contracts.StoredObject) (string, error) { diff --git a/internal/store/db/db.go b/internal/store/db/db.go index 211bfbd33..b5f661051 100644 --- a/internal/store/db/db.go +++ b/internal/store/db/db.go @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. + * Copyright (c) 2021 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 @@ -19,7 +20,6 @@ import "errors" const ( // Database providers - MongoDB = "mongodb" RedisDB = "redisdb" ) diff --git a/internal/store/db/redis/store.go b/internal/store/db/redis/store.go index 422ef1be0..89dda1237 100644 --- a/internal/store/db/redis/store.go +++ b/internal/store/db/redis/store.go @@ -1,5 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. + * Copyright (c) 2021 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,6 +26,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/redis/models" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" "github.com/gomodule/redigo/redis" @@ -43,7 +45,7 @@ type Client struct { // Store persists a stored object to the data store. Three ("Three shall be the number thou shalt // count, and the number of the counting shall be three") keys are used: -// * the object id to point to a STRING which is the marshal'ed JSON. +// * the object id to point to a STRING which is the marshalled JSON. // * the object AppServiceKey to point to a SET containing all object ids associated with this // app service. Note the key is prefixed to avoid key collisions. // * the object id to point to a HASH which contains the object AppServiceKey. @@ -54,7 +56,7 @@ func (c Client) Store(o contracts.StoredObject) (string, error) { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() exists, err := redis.Bool(conn.Do("EXISTS", o.ID)) if err != nil { @@ -95,7 +97,7 @@ func (c Client) RetrieveFromStore(appServiceKey string) (objects []contracts.Sto } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() ids, err := redis.Values(conn.Do("SMEMBERS", nameSpace+":idl:"+appServiceKey)) if err != nil { @@ -132,7 +134,7 @@ func (c Client) Update(o contracts.StoredObject) error { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() // retrieve the current AppServiceKey for this store object currentASK, err := redis.String(conn.Do("HGET", nameSpace+":ask:"+o.ID, "ASK")) @@ -174,7 +176,7 @@ func (c Client) RemoveFromStore(o contracts.StoredObject) error { } conn := c.Pool.Get() - defer conn.Close() + defer func() { _ = conn.Close() }() _ = conn.Send("MULTI") // remove the object's representation diff --git a/internal/store/factory.go b/internal/store/factory.go index 456c15a76..9708fe454 100644 --- a/internal/store/factory.go +++ b/internal/store/factory.go @@ -1,6 +1,6 @@ /******************************************************************************* * Copyright 2019 Dell Inc. - * Copyright 2020 Intel Inc. + * Copyright 2021 Intel Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at @@ -19,6 +19,7 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/store/db/redis" + bootstrapConfig "github.com/edgexfoundry/go-mod-bootstrap/v2/config" ) diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 8f58f460f..3cbdbec9b 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2019 Dell Inc., Intel Corporation + * Copyright 2021 Dell Inc., 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 @@ -46,7 +46,6 @@ type CpuUsage struct { Total uint64 // reported sum total of all usage } -var once sync.Once var lastSample CpuUsage var usageAvg float64 diff --git a/internal/telemetry/windows_cpu.go b/internal/telemetry/windows_cpu.go index 16c2d0254..ae07e1f55 100644 --- a/internal/telemetry/windows_cpu.go +++ b/internal/telemetry/windows_cpu.go @@ -1,7 +1,7 @@ // +build windows // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,8 +25,8 @@ import ( ) var ( - modkernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetSystemTimes = modkernel32.NewProc("GetSystemTimes") + modKernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetSystemTimes = modKernel32.NewProc("GetSystemTimes") ) func PollCpu() (cpuSnapshot CpuUsage) { @@ -69,7 +69,7 @@ func getSystemTimes(idleTime, kernelTime, userTime *FileTime) bool { return ret != 0 } -// FILETIME Struct from: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx +// FileTime Struct from: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724284.aspx type FileTime struct { // DwLowDateTime from Windows API LowDateTime uint32 diff --git a/internal/trigger/http/rest.go b/internal/trigger/http/rest.go index f551c5b31..02d8769ab 100644 --- a/internal/trigger/http/rest.go +++ b/internal/trigger/http/rest.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,70 +24,71 @@ import ( "net/http" "sync" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/webserver" ) // Trigger implements Trigger to support Triggers type Trigger struct { - Configuration *common.ConfigurationStruct - Runtime *runtime.GolangRuntime - outputData []byte - Webserver *webserver.WebServer - EdgeXClients common.EdgeXClients + dic *di.Container + Runtime *runtime.GolangRuntime + Webserver *webserver.WebServer + outputData []byte +} + +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime, webserver *webserver.WebServer) *Trigger { + return &Trigger{ + dic: dic, + Runtime: runtime, + Webserver: webserver, + } } // Initialize initializes the Trigger for logging and REST route -func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { - logger := trigger.EdgeXClients.LoggingClient +func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) if background != nil { return nil, errors.New("background publishing not supported for services using HTTP trigger") } - logger.Info("Initializing HTTP Trigger") + lc.Info("Initializing HTTP Trigger") trigger.Webserver.SetupTriggerRoute(internal.ApiTriggerRoute, trigger.requestHandler) - logger.Info("HTTP Trigger Initialized") + lc.Info("HTTP Trigger Initialized") return nil, nil } func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Request) { - defer r.Body.Close() + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) + defer func() { _ = r.Body.Close() }() - logger := trigger.EdgeXClients.LoggingClient contentType := r.Header.Get(clients.ContentType) data, err := ioutil.ReadAll(r.Body) if err != nil { - logger.Error("Error reading HTTP Body", "error", err) + lc.Error("Error reading HTTP Body", "error", err) writer.WriteHeader(http.StatusBadRequest) - writer.Write([]byte(fmt.Sprintf("Error reading HTTP Body: %s", err.Error()))) + _, _ = writer.Write([]byte(fmt.Sprintf("Error reading HTTP Body: %s", err.Error()))) return } - logger.Debug("Request Body read", "byte count", len(data)) + lc.Debug("Request Body read", "byte count", len(data)) correlationID := r.Header.Get(internal.CorrelationHeaderKey) - edgexContext := &appcontext.Context{ - CorrelationID: correlationID, - Configuration: trigger.Configuration, - LoggingClient: trigger.EdgeXClients.LoggingClient, - EventClient: trigger.EdgeXClients.EventClient, - ValueDescriptorClient: trigger.EdgeXClients.ValueDescriptorClient, - CommandClient: trigger.EdgeXClients.CommandClient, - NotificationsClient: trigger.EdgeXClients.NotificationsClient, - } - logger.Trace("Received message from http", clients.CorrelationHeader, correlationID) - logger.Debug("Received message from http", clients.ContentType, contentType) + appContext := appfunction.NewContext(correlationID, trigger.dic, contentType) + + lc.Trace("Received message from http", clients.CorrelationHeader, correlationID) + lc.Debug("Received message from http", clients.ContentType, contentType) envelope := types.MessageEnvelope{ CorrelationID: correlationID, @@ -95,21 +96,26 @@ func (trigger *Trigger) requestHandler(writer http.ResponseWriter, r *http.Reque Payload: data, } - messageError := trigger.Runtime.ProcessMessage(edgexContext, envelope) + messageError := trigger.Runtime.ProcessMessage(appContext, envelope) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. writer.WriteHeader(messageError.ErrorCode) - writer.Write([]byte(messageError.Err.Error())) + _, _ = writer.Write([]byte(messageError.Err.Error())) return } - if len(edgexContext.ResponseContentType) > 0 { - writer.Header().Set(clients.ContentType, edgexContext.ResponseContentType) + if len(appContext.ResponseContentType()) > 0 { + writer.Header().Set(clients.ContentType, appContext.ResponseContentType()) + } + + _, err = writer.Write(appContext.ResponseData()) + if err != nil { + lc.Errorf("unable to write ResponseData as HTTP response: %s", err.Error()) + return } - writer.Write(edgexContext.OutputData) - if edgexContext.OutputData != nil { - logger.Trace("Sent http response message", clients.CorrelationHeader, correlationID) + if appContext.ResponseData() != nil { + lc.Trace("Sent http response message", clients.CorrelationHeader, correlationID) } trigger.outputData = nil diff --git a/internal/trigger/http/rest_test.go b/internal/trigger/http/rest_test.go index cddb24366..b7bb3be27 100644 --- a/internal/trigger/http/rest_test.go +++ b/internal/trigger/http/rest_test.go @@ -1,5 +1,6 @@ // // Copyright (c) 2020 Technotects +// Copyright (c) 2021 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,6 +20,9 @@ package http import ( "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" "github.com/stretchr/testify/assert" @@ -26,7 +30,12 @@ import ( func TestTriggerInitializeWitBackgroundChannel(t *testing.T) { background := make(chan types.MessageEnvelope) - trigger := Trigger{} + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) + trigger := NewTrigger(dic, nil, nil) deferred, err := trigger.Initialize(nil, nil, background) diff --git a/internal/trigger/messagebus/messaging.go b/internal/trigger/messagebus/messaging.go index af43dce03..cf56d3704 100644 --- a/internal/trigger/messagebus/messaging.go +++ b/internal/trigger/messagebus/messaging.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,46 +22,53 @@ import ( "strings" "sync" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/messaging" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" ) // Trigger implements Trigger to support MessageBusData type Trigger struct { - Configuration *common.ConfigurationStruct - Runtime *runtime.GolangRuntime - client messaging.MessageClient - topics []types.TopicChannel - EdgeXClients common.EdgeXClients + dic *di.Container + runtime *runtime.GolangRuntime + topics []types.TopicChannel + client messaging.MessageClient +} + +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime) *Trigger { + return &Trigger{ + dic: dic, + runtime: runtime, + } } // Initialize ... func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { var err error - lc := trigger.EdgeXClients.LoggingClient + lc := bootstrapContainer.LoggingClientFrom(trigger.dic.Get) + config := container.ConfigurationFrom(trigger.dic.Get) - lc.Infof("Initializing Message Bus Trigger for '%s'", trigger.Configuration.Trigger.EdgexMessageBus.Type) + lc.Infof("Initializing Message Bus Trigger for '%s'", config.Trigger.EdgexMessageBus.Type) - trigger.client, err = messaging.NewMessageClient(trigger.Configuration.Trigger.EdgexMessageBus) + trigger.client, err = messaging.NewMessageClient(config.Trigger.EdgexMessageBus) if err != nil { return nil, err } - if len(strings.TrimSpace(trigger.Configuration.Trigger.SubscribeTopics)) == 0 { + if len(strings.TrimSpace(config.Trigger.SubscribeTopics)) == 0 { // Still allows subscribing to blank topic to receive all messages - trigger.topics = append(trigger.topics, types.TopicChannel{Topic: trigger.Configuration.Trigger.SubscribeTopics, Messages: make(chan types.MessageEnvelope)}) + trigger.topics = append(trigger.topics, types.TopicChannel{Topic: config.Trigger.SubscribeTopics, Messages: make(chan types.MessageEnvelope)}) } else { - topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(trigger.Configuration.Trigger.SubscribeTopics, util.SplitComma)) + topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(config.Trigger.SubscribeTopics, util.SplitComma)) for _, topic := range topics { trigger.topics = append(trigger.topics, types.TopicChannel{Topic: topic, Messages: make(chan types.MessageEnvelope)}) } @@ -75,17 +82,17 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context } lc.Infof("Subscribing to topic(s): '%s' @ %s://%s:%d", - trigger.Configuration.Trigger.SubscribeTopics, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Protocol, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Host, - trigger.Configuration.Trigger.EdgexMessageBus.SubscribeHost.Port) + config.Trigger.SubscribeTopics, + config.Trigger.EdgexMessageBus.SubscribeHost.Protocol, + config.Trigger.EdgexMessageBus.SubscribeHost.Host, + config.Trigger.EdgexMessageBus.SubscribeHost.Port) - if len(trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Host) > 0 { + if len(config.Trigger.EdgexMessageBus.PublishHost.Host) > 0 { lc.Infof("Publishing to topic: '%s' @ %s://%s:%d", - trigger.Configuration.Trigger.PublishTopic, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Protocol, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Host, - trigger.Configuration.Trigger.EdgexMessageBus.PublishHost.Port) + config.Trigger.PublishTopic, + config.Trigger.EdgexMessageBus.PublishHost.Protocol, + config.Trigger.EdgexMessageBus.PublishHost.Host, + config.Trigger.EdgexMessageBus.PublishHost.Port) } // Need to have a go func for each subscription so we know with topic the data was received for. @@ -122,13 +129,13 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context case bg := <-background: go func() { - err := trigger.client.Publish(bg, trigger.Configuration.Trigger.PublishTopic) + err := trigger.client.Publish(bg, config.Trigger.PublishTopic) if err != nil { lc.Errorf("Failed to publish background Message to bus, %v", err) return } - lc.Debugf("Published background message to bus on %s topic", trigger.Configuration.Trigger.PublishTopic) + lc.Debugf("Published background message to bus on %s topic", config.Trigger.PublishTopic) lc.Tracef("%s=%s", clients.CorrelationHeader, bg.CorrelationID) }() } @@ -136,7 +143,7 @@ func (trigger *Trigger) Initialize(appWg *sync.WaitGroup, appCtx context.Context }() if err := trigger.client.Subscribe(trigger.topics, messageErrors); err != nil { - return nil, fmt.Errorf("failed to subscribe to topic(s) '%s': %s", trigger.Configuration.Trigger.SubscribeTopics, err.Error()) + return nil, fmt.Errorf("failed to subscribe to topic(s) '%s': %s", config.Trigger.SubscribeTopics, err.Error()) } deferred := func() { @@ -153,46 +160,41 @@ func (trigger *Trigger) processMessage(logger logger.LoggingClient, triggerTopic logger.Debugf("Received message from MessageBus on topic '%s'. Content-Type=%s", triggerTopic.Topic, message.ContentType) logger.Tracef("%s=%s", clients.CorrelationHeader, message.CorrelationID) - edgexContext := &appcontext.Context{ - CorrelationID: message.CorrelationID, - Configuration: trigger.Configuration, - LoggingClient: trigger.EdgeXClients.LoggingClient, - EventClient: trigger.EdgeXClients.EventClient, - ValueDescriptorClient: trigger.EdgeXClients.ValueDescriptorClient, - CommandClient: trigger.EdgeXClients.CommandClient, - NotificationsClient: trigger.EdgeXClients.NotificationsClient, - } + appContext := appfunction.NewContext(message.CorrelationID, trigger.dic, message.ContentType) - messageError := trigger.Runtime.ProcessMessage(edgexContext, message) + messageError := trigger.runtime.ProcessMessage(appContext, message) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. return } - if edgexContext.OutputData != nil { + if appContext.ResponseData() != nil { var contentType string - if edgexContext.ResponseContentType != "" { - contentType = edgexContext.ResponseContentType + if appContext.ResponseContentType() != "" { + contentType = appContext.ResponseContentType() } else { contentType = clients.ContentTypeJSON - if edgexContext.OutputData[0] != byte('{') { + if appContext.ResponseData()[0] != byte('{') { // If not JSON then assume it is CBOR contentType = clients.ContentTypeCBOR } } outputEnvelope := types.MessageEnvelope{ - CorrelationID: edgexContext.CorrelationID, - Payload: edgexContext.OutputData, + CorrelationID: appContext.CorrelationID(), + Payload: appContext.ResponseData(), ContentType: contentType, } - err := trigger.client.Publish(outputEnvelope, trigger.Configuration.Trigger.PublishTopic) + + config := container.ConfigurationFrom(trigger.dic.Get) + + err := trigger.client.Publish(outputEnvelope, config.Trigger.PublishTopic) if err != nil { logger.Errorf("Failed to publish Message to bus, %v", err) return } - logger.Debugf("Published message to bus on '%s' topic", trigger.Configuration.Trigger.PublishTopic) + logger.Debugf("Published message to bus on '%s' topic", config.Trigger.PublishTopic) logger.Tracef("%s=%s", clients.CorrelationHeader, message.CorrelationID) } } diff --git a/internal/trigger/messagebus/messaging_test.go b/internal/trigger/messagebus/messaging_test.go index a99472c43..2128d2fe8 100644 --- a/internal/trigger/messagebus/messaging_test.go +++ b/internal/trigger/messagebus/messaging_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,11 +23,14 @@ import ( "testing" "time" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" @@ -44,8 +47,6 @@ import ( // Note the constant TriggerTypeMessageBus can not be used due to cyclic imports const TriggerTypeMessageBus = "EDGEX-MESSAGEBUS" -var lc logger.LoggingClient - var addEventRequest = createTestEventRequest() var expectedEvent = addEventRequest.Event @@ -56,8 +57,14 @@ func createTestEventRequest() requests.AddEventRequest { return request } +var dic *di.Container + func TestMain(m *testing.M) { - lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + }) m.Run() } @@ -85,10 +92,18 @@ func TestInitialize(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + goRuntime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + trigger := NewTrigger(dic, goRuntime) + + _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) assert.NotNil(t, trigger.client, "Expected client to be set") assert.Equal(t, 1, len(trigger.topics)) assert.Equal(t, "events", trigger.topics[0].Topic) @@ -119,9 +134,15 @@ func TestInitializeBadConfiguration(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + goRuntime := &runtime.GolangRuntime{} - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + trigger := NewTrigger(dic, goRuntime) _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) assert.Error(t, err) } @@ -149,21 +170,28 @@ func TestInitializeAndProcessEventWithNoOutput(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) + _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) payload, err := json.Marshal(addEventRequest) require.NoError(t, err) @@ -185,13 +213,16 @@ func TestInitializeAndProcessEventWithNoOutput(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) require.NoError(t, err, "Unable to create to publisher") - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "") //transform1 should be called after this executes require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - assert.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } } func TestInitializeAndProcessEventWithOutput(t *testing.T) { @@ -217,25 +248,31 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + responseContentType := uuid.New().String() expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.ResponseContentType = responseContentType - edgexcontext.Complete([]byte("Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseContentType(responseContentType) + appContext.SetResponseData([]byte("Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -253,7 +290,7 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -270,13 +307,15 @@ func TestInitializeAndProcessEventWithOutput(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") - + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true for receiveMessage { @@ -315,22 +354,28 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.Complete([]byte("{;)Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseData([]byte("{;)Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -348,7 +393,7 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -365,12 +410,15 @@ func TestInitializeAndProcessEventWithOutput_InferJSON(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true @@ -410,22 +458,27 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - transformWasCalled := common.AtomicBool{} + transformWasCalled := make(chan bool, 1) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - transformWasCalled.Set(true) - assert.Equal(t, expectedEvent, params[0]) - edgexcontext.Complete([]byte("Transformed")) //transformed message published to message bus + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + assert.Equal(t, expectedEvent, data) + appContext.SetResponseData([]byte("Transformed")) //transformed message published to message bus + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} - + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ Host: "localhost", @@ -442,12 +495,13 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus require.NoError(t, err) - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + _, err = trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) + require.NoError(t, err) payload, _ := json.Marshal(addEventRequest) @@ -457,12 +511,15 @@ func TestInitializeAndProcessEventWithOutput_AssumeCBOR(t *testing.T) { ContentType: clients.ContentTypeJSON, } - assert.False(t, transformWasCalled.Value()) err = testClient.Publish(message, "SubscribeTopic") require.NoError(t, err, "Failed to publish message") - time.Sleep(3 * time.Second) - require.True(t, transformWasCalled.Value(), "Transform never called") + select { + case <-transformWasCalled: + // do nothing, just need to fall out. + case <-time.After(3 * time.Second): + require.Fail(t, "Transform never called") + } receiveMessage := true @@ -502,13 +559,19 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" expectedPayload := []byte(`{"id":"5888dea1bd36573f4681d6f9","created":1485364897029,"modified":1485364897029,"origin":1471806386919,"pushed":0,"device":"livingroomthermostat","readings":[{"id":"5888dea0bd36573f4681d6f8","created":1485364896983,"modified":1485364896983,"origin":1471806386919,"pushed":0,"name":"temperature","value":"38","device":"livingroomthermostat"}]}`) goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + trigger := NewTrigger(dic, goRuntime) testClientConfig := types.MessageBusConfig{ SubscribeHost: types.HostInfo{ @@ -526,7 +589,7 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { testClient, err := messaging.NewMessageClient(testClientConfig) //new client to publish & subscribe require.NoError(t, err, "Failed to create test client") - testTopics := []types.TopicChannel{{Topic: trigger.Configuration.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} + testTopics := []types.TopicChannel{{Topic: config.Trigger.PublishTopic, Messages: make(chan types.MessageEnvelope)}} testMessageErrors := make(chan error) err = testClient.Subscribe(testTopics, testMessageErrors) //subscribe in order to receive transformed output to the bus @@ -534,7 +597,8 @@ func TestInitializeAndProcessBackgroundMessage(t *testing.T) { background := make(chan types.MessageEnvelope) - _, _ = trigger.Initialize(&sync.WaitGroup{}, context.Background(), background) + _, err = trigger.Initialize(&sync.WaitGroup{}, context.Background(), background) + require.NoError(t, err) message := types.MessageEnvelope{ CorrelationID: expectedCorrelationID, @@ -580,19 +644,25 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { }, } + dic.Update(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return &config + }, + }) + expectedCorrelationID := "123" - done := make(chan bool) - transform1 := func(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - require.Equal(t, expectedEvent, params[0]) - done <- true + transformWasCalled := make(chan bool, 1) + transform1 := func(appContext interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + require.Equal(t, expectedEvent, data) + transformWasCalled <- true return false, nil } goRuntime := &runtime.GolangRuntime{} - goRuntime.Initialize(nil, nil) - goRuntime.SetTransforms([]appcontext.AppFunction{transform1}) - trigger := Trigger{Configuration: &config, Runtime: goRuntime, EdgeXClients: common.EdgeXClients{LoggingClient: lc}} + goRuntime.Initialize(dic) + goRuntime.SetTransforms([]interfaces.AppFunction{transform1}) + trigger := NewTrigger(dic, goRuntime) _, err := trigger.Initialize(&sync.WaitGroup{}, context.Background(), nil) require.NoError(t, err) @@ -620,7 +690,7 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { require.NoError(t, err, "Failed to publish message") select { - case <-done: + case <-transformWasCalled: // do nothing, just need to fall out. case <-time.After(3 * time.Second): require.Fail(t, "Transform never called for t1") @@ -630,7 +700,7 @@ func TestInitializeAndProcessEventMultipleTopics(t *testing.T) { require.NoError(t, err, "Failed to publish message") select { - case <-done: + case <-transformWasCalled: // do nothing, just need to fall out. case <-time.After(3 * time.Second): require.Fail(t, "Transform never called t2") diff --git a/internal/trigger/mqtt/mqtt.go b/internal/trigger/mqtt/mqtt.go index 167aec735..f89394729 100644 --- a/internal/trigger/mqtt/mqtt.go +++ b/internal/trigger/mqtt/mqtt.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,50 +25,48 @@ import ( "sync" "time" - pahoMqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" - "github.com/google/uuid" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/runtime" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" - "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + pahoMqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/google/uuid" ) // Trigger implements Trigger to support Triggers type Trigger struct { - configuration *common.ConfigurationStruct - mqttClient pahoMqtt.Client - runtime *runtime.GolangRuntime - edgeXClients common.EdgeXClients - secretProvider interfaces.SecretProvider + dic *di.Container + lc logger.LoggingClient + mqttClient pahoMqtt.Client + runtime *runtime.GolangRuntime } -func NewTrigger( - configuration *common.ConfigurationStruct, - runtime *runtime.GolangRuntime, - clients common.EdgeXClients, - secretProvider interfaces.SecretProvider) *Trigger { +func NewTrigger(dic *di.Container, runtime *runtime.GolangRuntime) *Trigger { return &Trigger{ - configuration: configuration, - runtime: runtime, - edgeXClients: clients, - secretProvider: secretProvider, + dic: dic, + runtime: runtime, + lc: bootstrapContainer.LoggingClientFrom(dic.Get), } } // Initialize initializes the Trigger for an external MQTT broker func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - brokerConfig := trigger.configuration.Trigger.ExternalMqtt - topics := trigger.configuration.Trigger.SubscribeTopics + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + brokerConfig := config.Trigger.ExternalMqtt + topics := config.Trigger.SubscribeTopics - logger.Info("Initializing MQTT Trigger") + lc.Info("Initializing MQTT Trigger") if background != nil { return nil, errors.New("background publishing not supported for services using MQTT trigger") @@ -80,7 +78,7 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro brokerUrl, err := url.Parse(brokerConfig.Url) if err != nil { - return nil, fmt.Errorf("invalid MQTT Broker Url '%s': %s", trigger.configuration.Trigger.ExternalMqtt.Url, err.Error()) + return nil, fmt.Errorf("invalid MQTT Broker Url '%s': %s", config.Trigger.ExternalMqtt.Url, err.Error()) } opts := pahoMqtt.NewClientOptions() @@ -97,9 +95,10 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro opts.KeepAlive = brokerConfig.KeepAlive opts.Servers = []*url.URL{brokerUrl} + // Since this factory is shared between the MQTT pipeline function and this trigger we must provide + // a dummy AppFunctionContext which will provide access to GetSecret mqttFactory := secure.NewMqttFactory( - logger, - trigger.secretProvider, + appfunction.NewContext("", trigger.dic, ""), brokerConfig.AuthMode, brokerConfig.SecretPath, brokerConfig.SkipCertVerify, @@ -110,16 +109,16 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro return nil, fmt.Errorf("unable to create secure MQTT Client: %s", err.Error()) } - logger.Info(fmt.Sprintf("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl)) + lc.Info(fmt.Sprintf("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl)) if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { return nil, fmt.Errorf("could not connect to broker for MQTT trigger: %s", token.Error().Error()) } - logger.Info("Connected to mqtt server for MQTT trigger") + lc.Info("Connected to mqtt server for MQTT trigger") deferred := func() { - logger.Info("Disconnecting from broker for MQTT trigger") + lc.Info("Disconnecting from broker for MQTT trigger") trigger.mqttClient.Disconnect(0) } @@ -130,27 +129,29 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro func (trigger *Trigger) onConnectHandler(mqttClient pahoMqtt.Client) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(trigger.configuration.Trigger.SubscribeTopics, util.SplitComma)) - qos := trigger.configuration.Trigger.ExternalMqtt.QoS + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + topics := util.DeleteEmptyAndTrim(strings.FieldsFunc(config.Trigger.SubscribeTopics, util.SplitComma)) + qos := config.Trigger.ExternalMqtt.QoS for _, topic := range topics { if token := mqttClient.Subscribe(topic, qos, trigger.messageHandler); token.Wait() && token.Error() != nil { mqttClient.Disconnect(0) - logger.Error(fmt.Sprintf("could not subscribe to topic '%s' for MQTT trigger: %s", + lc.Error(fmt.Sprintf("could not subscribe to topic '%s' for MQTT trigger: %s", topic, token.Error().Error())) return } } - logger.Infof("Subscribed to topic(s) '%s' for MQTT trigger", trigger.configuration.Trigger.SubscribeTopics) + lc.Infof("Subscribed to topic(s) '%s' for MQTT trigger", config.Trigger.SubscribeTopics) } func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt.Message) { // Convenience short cuts - logger := trigger.edgeXClients.LoggingClient - brokerConfig := trigger.configuration.Trigger.ExternalMqtt - topic := trigger.configuration.Trigger.PublishTopic + lc := trigger.lc + config := container.ConfigurationFrom(trigger.dic.Get) + brokerConfig := config.Trigger.ExternalMqtt + topic := config.Trigger.PublishTopic data := message.Payload() contentType := clients.ContentTypeJSON @@ -161,18 +162,10 @@ func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt. correlationID := uuid.New().String() - edgexContext := &appcontext.Context{ - CorrelationID: correlationID, - Configuration: trigger.configuration, - LoggingClient: trigger.edgeXClients.LoggingClient, - EventClient: trigger.edgeXClients.EventClient, - ValueDescriptorClient: trigger.edgeXClients.ValueDescriptorClient, - CommandClient: trigger.edgeXClients.CommandClient, - NotificationsClient: trigger.edgeXClients.NotificationsClient, - } + appContext := appfunction.NewContext(correlationID, trigger.dic, contentType) - logger.Debugf("Received message from MQTT Trigger with %d bytes from topic '%s'. Content-Type=%s", len(data), message.Topic(), contentType) - logger.Tracef("%s=%s", clients.CorrelationHeader, correlationID) + lc.Debugf("Received message from MQTT Trigger with %d bytes from topic '%s'. Content-Type=%s", len(data), message.Topic(), contentType) + lc.Tracef("%s=%s", clients.CorrelationHeader, correlationID) envelope := types.MessageEnvelope{ CorrelationID: correlationID, @@ -180,19 +173,19 @@ func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt. Payload: data, } - messageError := trigger.runtime.ProcessMessage(edgexContext, envelope) + messageError := trigger.runtime.ProcessMessage(appContext, envelope) if messageError != nil { // ProcessMessage logs the error, so no need to log it here. // ToDo: Do we want to publish the error back to the Broker? return } - if len(edgexContext.OutputData) > 0 && len(topic) > 0 { - if token := client.Publish(topic, brokerConfig.QoS, brokerConfig.Retain, edgexContext.OutputData); token.Wait() && token.Error() != nil { - logger.Error("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) + if len(appContext.ResponseData()) > 0 && len(topic) > 0 { + if token := client.Publish(topic, brokerConfig.QoS, brokerConfig.Retain, appContext.ResponseData); token.Wait() && token.Error() != nil { + lc.Error("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) } else { - logger.Trace("Sent MQTT Trigger response message", clients.CorrelationHeader, correlationID) - logger.Debug(fmt.Sprintf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(edgexContext.OutputData))) + lc.Trace("Sent MQTT Trigger response message", clients.CorrelationHeader, correlationID) + lc.Debug(fmt.Sprintf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(appContext.ResponseData()))) } } } diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 77974fa4b..ae82f1de3 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,10 +21,12 @@ import ( "net/http" "time" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" contracts "github.com/edgexfoundry/go-mod-core-contracts/v2/v2" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/controller/rest" @@ -35,11 +37,10 @@ import ( // WebServer handles the webserver configuration type WebServer struct { - Config *common.ConfigurationStruct - lc logger.LoggingClient - router *mux.Router - secretProvider interfaces.SecretProvider - controller *rest.Controller + config *common.ConfigurationStruct + lc logger.LoggingClient + router *mux.Router + controller *rest.Controller } // swagger:model @@ -49,13 +50,12 @@ type Version struct { } // NewWebserver returns a new instance of *WebServer -func NewWebServer(config *common.ConfigurationStruct, secretProvider interfaces.SecretProvider, lc logger.LoggingClient, router *mux.Router) *WebServer { +func NewWebServer(dic *di.Container, router *mux.Router) *WebServer { ws := &WebServer{ - Config: config, - lc: lc, - router: router, - secretProvider: secretProvider, - controller: rest.NewController(router, lc, config, secretProvider), + lc: bootstrapContainer.LoggingClientFrom(dic.Get), + config: container.ConfigurationFrom(dic.Get), + router: router, + controller: rest.NewController(router, dic), } return ws @@ -95,7 +95,7 @@ func (webserver *WebServer) SetupTriggerRoute(path string, handlerForTrigger fun // StartWebServer starts the web server func (webserver *WebServer) StartWebServer(errChannel chan error) { go func() { - if serviceTimeout, err := time.ParseDuration(webserver.Config.Service.Timeout); err != nil { + if serviceTimeout, err := time.ParseDuration(webserver.config.Service.Timeout); err != nil { errChannel <- fmt.Errorf("failed to parse Service.Timeout: %v", err) } else { listenAndServe(webserver, serviceTimeout, errChannel) @@ -108,11 +108,11 @@ func listenAndServe(webserver *WebServer, serviceTimeout time.Duration, errChann // this allows env overrides to explicitly set the value used // for ListenAndServe, as needed for different deployments - addr := fmt.Sprintf("%v:%d", webserver.Config.Service.ServerBindAddr, webserver.Config.Service.Port) + addr := fmt.Sprintf("%v:%d", webserver.config.Service.ServerBindAddr, webserver.config.Service.Port) - if webserver.Config.Service.Protocol == "https" { + if webserver.config.Service.Protocol == "https" { webserver.lc.Infof("Starting HTTPS Web Server on address %v", addr) - errChannel <- http.ListenAndServeTLS(addr, webserver.Config.Service.HTTPSCert, webserver.Config.Service.HTTPSKey, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) + errChannel <- http.ListenAndServeTLS(addr, webserver.config.Service.HTTPSCert, webserver.config.Service.HTTPSKey, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) } else { webserver.lc.Infof("Starting HTTP Web Server on address %v", addr) errChannel <- http.ListenAndServe(addr, http.TimeoutHandler(webserver.router, serviceTimeout, "Request timed out")) diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index 3f945a00a..5a9e64d19 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,32 +19,45 @@ package webserver import ( "net/http" "net/http/httptest" + "os" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" ) -var logClient logger.LoggingClient -var config *common.ConfigurationStruct +var dic *di.Container func TestMain(m *testing.M) { - logClient = logger.NewMockClient() - config = &common.ConfigurationStruct{} - m.Run() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return logger.NewMockClient() + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return &mocks.SecretProvider{} + }, + }) + + os.Exit(m.Run()) } func TestAddRoute(t *testing.T) { routePath := "/testRoute" testHandler := func(_ http.ResponseWriter, _ *http.Request) {} - sp := &mocks.SecretProvider{} - webserver := NewWebServer(config, sp, logClient, mux.NewRouter()) + webserver := NewWebServer(dic, mux.NewRouter()) err := webserver.AddRoute(routePath, testHandler) assert.NoError(t, err, "Not expecting an error") @@ -55,13 +68,13 @@ func TestAddRoute(t *testing.T) { } func TestSetupTriggerRoute(t *testing.T) { - sp := &mocks.SecretProvider{} - webserver := NewWebServer(config, sp, logClient, mux.NewRouter()) + webserver := NewWebServer(dic, mux.NewRouter()) handlerFunctionNotCalled := true handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("test")) + _, err := w.Write([]byte("test")) + require.NoError(t, err) handlerFunctionNotCalled = false } diff --git a/pkg/factory.go b/pkg/factory.go new file mode 100644 index 000000000..1fc7030be --- /dev/null +++ b/pkg/factory.go @@ -0,0 +1,61 @@ +// +// Copyright (c) 2021 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 pkg + +import ( + "fmt" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/app" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" +) + +// NewAppService creates and returns a new ApplicationService with the default TargetType +func NewAppService(serviceKey string) (interfaces.ApplicationService, bool) { + return NewAppServiceWithTargetType(serviceKey, nil) +} + +// NewAppService creates and returns a new ApplicationService with the specified TargetType +func NewAppServiceWithTargetType(serviceKey string, targetType interface{}) (interfaces.ApplicationService, bool) { + service := app.NewService(serviceKey, targetType, interfaces.ProfileSuffixPlaceholder) + if err := service.Initialize(); err != nil { + err = fmt.Errorf("initialization failed: %s", err.Error()) + service.LoggingClient().Errorf("App Service %s", err.Error()) + return nil, false + } + + return service, true +} + +// NewAppFuncContextForTest creates and returns a new AppFunctionContext to be used in unit tests for custom pipeline functions +func NewAppFuncContextForTest(correlationID string, lc logger.LoggingClient) interfaces.AppFunctionContext { + dic := di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + container.ConfigurationName: func(get di.Get) interface{} { + return &common.ConfigurationStruct{} + }, + }) + return appfunction.NewContext(correlationID, dic, "") +} diff --git a/pkg/factory_test.go b/pkg/factory_test.go new file mode 100644 index 000000000..2e45b3d01 --- /dev/null +++ b/pkg/factory_test.go @@ -0,0 +1,56 @@ +// +// Copyright (c) 2021 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 pkg + +import ( + "os" + "testing" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +var expectedLogger logger.LoggingClient +var expectedCorrelationId string + +var dic *di.Container + +func TestMain(m *testing.M) { + expectedCorrelationId = uuid.NewString() + expectedLogger = logger.NewMockClient() + + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return expectedLogger + }, + }) + + os.Exit(m.Run()) +} + +func TestNewAppFuncContextForTest(t *testing.T) { + expectedContentType := "" + + target := NewAppFuncContextForTest(expectedCorrelationId, expectedLogger) + + assert.Equal(t, expectedLogger, target.LoggingClient()) + assert.Equal(t, expectedCorrelationId, target.CorrelationID()) + assert.Equal(t, expectedContentType, target.InputContentType()) +} diff --git a/pkg/interfaces/backgroundpublisher.go b/pkg/interfaces/backgroundpublisher.go new file mode 100644 index 000000000..a07fb820b --- /dev/null +++ b/pkg/interfaces/backgroundpublisher.go @@ -0,0 +1,25 @@ +// +// Copyright (c) 2020 Technotects +// Copyright (c) 2021 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 interfaces + +// BackgroundPublisher provides an interface to send messages from background processes +// through the service's configured MessageBus output +type BackgroundPublisher interface { + // Publish provided message through the configured MessageBus output + Publish(payload []byte, correlationID string, contentType string) +} diff --git a/pkg/interfaces/context.go b/pkg/interfaces/context.go new file mode 100644 index 000000000..376b336d1 --- /dev/null +++ b/pkg/interfaces/context.go @@ -0,0 +1,77 @@ +// +// Copyright (c) 2021 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 interfaces + +import ( + "time" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" +) + +// AppFunction is a type alias for a application pipeline function. +// appCtx is a reference to the AppFunctionContext below. +// data is the data to be operated on by the function. +// bool return value indicates if the pipeline should continue executing (true) or not (false) +// interface{} is either the data to pass to the next function (continue executing) or +// an error (stop executing due to error) or nil (done executing) +type AppFunction = func(appCxt AppFunctionContext, data interface{}) (bool, interface{}) + +// AppFunctionContext defines the interface for an Edgex Application Service Context provided to +// App Functions when executing in the Functions Pipeline. +type AppFunctionContext interface { + // CorrelationID returns the correlation ID associated with the context. + CorrelationID() string + // InputContentType returns the content type of the data that initiated the pipeline execution. Only useful when + // the TargetType for the pipeline is []byte, otherwise the data with be the type specified by TargetType. + InputContentType() string + // SetResponseData sets the response data that will be returned to the trigger when pipeline execution is complete. + SetResponseData(data []byte) + // SetResponseContentType sets the content type that will be returned to the trigger when pipeline + // execution is complete. + SetResponseContentType(string) + // ResponseContentType returns the content type that will be returned to the trigger when pipeline + // execution is complete. + ResponseContentType() string + // SetRetryData set the data that is to be retried later as part of the Store and Forward capability. + // Used when there was failure sending the data to an external source. + SetRetryData(data []byte) + // GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. + // An error is returned if the path is not found or any of the keys (if specified) are not found. + // Omit keys if all secret data for the specified path is required. + GetSecret(path string, keys ...string) (map[string]string, error) + // SecretsLastUpdated returns that timestamp for when the secrets in the SecretStore where last updated. + // Useful when a connection to external source needs to be redone when the credentials have been updated. + SecretsLastUpdated() time.Time + // LoggingClient returns the Logger client + LoggingClient() logger.LoggingClient + // EventClient returns the Event client. Note if Core Data is not specified in the Clients configuration, + // this will return nil. + EventClient() coredata.EventClient + // CommandClient returns the Command client. Note if Support Command is not specified in the Clients configuration, + // this will return nil. + CommandClient() command.CommandClient + // NotificationsClient returns the Notifications client. Note if Support Notifications is not specified in the + // Clients configuration, this will return nil. + NotificationsClient() notifications.NotificationsClient + // PushToCoreData is a convenience function for adding new Event/Reading(s) to core data and + // back onto the EdgeX MessageBus. This function uses the Event client and will result in an error if + // Core Data is not specified in the Clients configuration + PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) +} diff --git a/pkg/interfaces/mocks/AppFunctionContext.go b/pkg/interfaces/mocks/AppFunctionContext.go new file mode 100644 index 000000000..646c33e70 --- /dev/null +++ b/pkg/interfaces/mocks/AppFunctionContext.go @@ -0,0 +1,211 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + command "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + coredata "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + + dtos "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + logger "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + mock "github.com/stretchr/testify/mock" + + notifications "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + time "time" +) + +// AppFunctionContext is an autogenerated mock type for the AppFunctionContext type +type AppFunctionContext struct { + mock.Mock +} + +// CommandClient provides a mock function with given fields: +func (_m *AppFunctionContext) CommandClient() command.CommandClient { + ret := _m.Called() + + var r0 command.CommandClient + if rf, ok := ret.Get(0).(func() command.CommandClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(command.CommandClient) + } + } + + return r0 +} + +// CorrelationID provides a mock function with given fields: +func (_m *AppFunctionContext) CorrelationID() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// EventClient provides a mock function with given fields: +func (_m *AppFunctionContext) EventClient() coredata.EventClient { + ret := _m.Called() + + var r0 coredata.EventClient + if rf, ok := ret.Get(0).(func() coredata.EventClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(coredata.EventClient) + } + } + + return r0 +} + +// GetSecret provides a mock function with given fields: path, keys +func (_m *AppFunctionContext) GetSecret(path string, keys ...string) (map[string]string, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, path) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(string, ...string) map[string]string); ok { + r0 = rf(path, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(path, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InputContentType provides a mock function with given fields: +func (_m *AppFunctionContext) InputContentType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// LoggingClient provides a mock function with given fields: +func (_m *AppFunctionContext) LoggingClient() logger.LoggingClient { + ret := _m.Called() + + var r0 logger.LoggingClient + if rf, ok := ret.Get(0).(func() logger.LoggingClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.LoggingClient) + } + } + + return r0 +} + +// NotificationsClient provides a mock function with given fields: +func (_m *AppFunctionContext) NotificationsClient() notifications.NotificationsClient { + ret := _m.Called() + + var r0 notifications.NotificationsClient + if rf, ok := ret.Get(0).(func() notifications.NotificationsClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(notifications.NotificationsClient) + } + } + + return r0 +} + +// PushToCoreData provides a mock function with given fields: deviceName, readingName, value +func (_m *AppFunctionContext) PushToCoreData(deviceName string, readingName string, value interface{}) (*dtos.Event, error) { + ret := _m.Called(deviceName, readingName, value) + + var r0 *dtos.Event + if rf, ok := ret.Get(0).(func(string, string, interface{}) *dtos.Event); ok { + r0 = rf(deviceName, readingName, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dtos.Event) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, interface{}) error); ok { + r1 = rf(deviceName, readingName, value) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ResponseContentType provides a mock function with given fields: +func (_m *AppFunctionContext) ResponseContentType() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// SecretsLastUpdated provides a mock function with given fields: +func (_m *AppFunctionContext) SecretsLastUpdated() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// SetResponseContentType provides a mock function with given fields: _a0 +func (_m *AppFunctionContext) SetResponseContentType(_a0 string) { + _m.Called(_a0) +} + +// SetResponseData provides a mock function with given fields: data +func (_m *AppFunctionContext) SetResponseData(data []byte) { + _m.Called(data) +} + +// SetRetryData provides a mock function with given fields: data +func (_m *AppFunctionContext) SetRetryData(data []byte) { + _m.Called(data) +} diff --git a/pkg/interfaces/mocks/ApplicationService.go b/pkg/interfaces/mocks/ApplicationService.go new file mode 100644 index 000000000..7f9f8a6f0 --- /dev/null +++ b/pkg/interfaces/mocks/ApplicationService.go @@ -0,0 +1,301 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + command "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + coredata "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + + http "net/http" + + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + + logger "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + mock "github.com/stretchr/testify/mock" + + notifications "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + + registry "github.com/edgexfoundry/go-mod-registry/v2/registry" +) + +// ApplicationService is an autogenerated mock type for the ApplicationService type +type ApplicationService struct { + mock.Mock +} + +// AddBackgroundPublisher provides a mock function with given fields: capacity +func (_m *ApplicationService) AddBackgroundPublisher(capacity int) interfaces.BackgroundPublisher { + ret := _m.Called(capacity) + + var r0 interfaces.BackgroundPublisher + if rf, ok := ret.Get(0).(func(int) interfaces.BackgroundPublisher); ok { + r0 = rf(capacity) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interfaces.BackgroundPublisher) + } + } + + return r0 +} + +// AddRoute provides a mock function with given fields: route, handler, methods +func (_m *ApplicationService) AddRoute(route string, handler func(http.ResponseWriter, *http.Request), methods ...string) error { + _va := make([]interface{}, len(methods)) + for _i := range methods { + _va[_i] = methods[_i] + } + var _ca []interface{} + _ca = append(_ca, route, handler) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func(http.ResponseWriter, *http.Request), ...string) error); ok { + r0 = rf(route, handler, methods...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ApplicationSettings provides a mock function with given fields: +func (_m *ApplicationService) ApplicationSettings() map[string]string { + ret := _m.Called() + + var r0 map[string]string + if rf, ok := ret.Get(0).(func() map[string]string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + return r0 +} + +// CommandClient provides a mock function with given fields: +func (_m *ApplicationService) CommandClient() command.CommandClient { + ret := _m.Called() + + var r0 command.CommandClient + if rf, ok := ret.Get(0).(func() command.CommandClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(command.CommandClient) + } + } + + return r0 +} + +// EventClient provides a mock function with given fields: +func (_m *ApplicationService) EventClient() coredata.EventClient { + ret := _m.Called() + + var r0 coredata.EventClient + if rf, ok := ret.Get(0).(func() coredata.EventClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(coredata.EventClient) + } + } + + return r0 +} + +// GetAppSettingStrings provides a mock function with given fields: setting +func (_m *ApplicationService) GetAppSettingStrings(setting string) ([]string, error) { + ret := _m.Called(setting) + + var r0 []string + if rf, ok := ret.Get(0).(func(string) []string); ok { + r0 = rf(setting) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(setting) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSecret provides a mock function with given fields: path, keys +func (_m *ApplicationService) GetSecret(path string, keys ...string) (map[string]string, error) { + _va := make([]interface{}, len(keys)) + for _i := range keys { + _va[_i] = keys[_i] + } + var _ca []interface{} + _ca = append(_ca, path) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]string + if rf, ok := ret.Get(0).(func(string, ...string) map[string]string); ok { + r0 = rf(path, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(path, keys...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoadConfigurablePipeline provides a mock function with given fields: +func (_m *ApplicationService) LoadConfigurablePipeline() ([]func(interfaces.AppFunctionContext, interface{}) (bool, interface{}), error) { + ret := _m.Called() + + var r0 []func(interfaces.AppFunctionContext, interface{}) (bool, interface{}) + if rf, ok := ret.Get(0).(func() []func(interfaces.AppFunctionContext, interface{}) (bool, interface{})); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoggingClient provides a mock function with given fields: +func (_m *ApplicationService) LoggingClient() logger.LoggingClient { + ret := _m.Called() + + var r0 logger.LoggingClient + if rf, ok := ret.Get(0).(func() logger.LoggingClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(logger.LoggingClient) + } + } + + return r0 +} + +// MakeItRun provides a mock function with given fields: +func (_m *ApplicationService) MakeItRun() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MakeItStop provides a mock function with given fields: +func (_m *ApplicationService) MakeItStop() { + _m.Called() +} + +// NotificationsClient provides a mock function with given fields: +func (_m *ApplicationService) NotificationsClient() notifications.NotificationsClient { + ret := _m.Called() + + var r0 notifications.NotificationsClient + if rf, ok := ret.Get(0).(func() notifications.NotificationsClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(notifications.NotificationsClient) + } + } + + return r0 +} + +// RegisterCustomTriggerFactory provides a mock function with given fields: name, factory +func (_m *ApplicationService) RegisterCustomTriggerFactory(name string, factory func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error { + ret := _m.Called(name, factory) + + var r0 error + if rf, ok := ret.Get(0).(func(string, func(interfaces.TriggerConfig) (interfaces.Trigger, error)) error); ok { + r0 = rf(name, factory) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RegistryClient provides a mock function with given fields: +func (_m *ApplicationService) RegistryClient() registry.Client { + ret := _m.Called() + + var r0 registry.Client + if rf, ok := ret.Get(0).(func() registry.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(registry.Client) + } + } + + return r0 +} + +// SetFunctionsPipeline provides a mock function with given fields: transforms +func (_m *ApplicationService) SetFunctionsPipeline(transforms ...func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) error { + _va := make([]interface{}, len(transforms)) + for _i := range transforms { + _va[_i] = transforms[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...func(interfaces.AppFunctionContext, interface{}) (bool, interface{})) error); ok { + r0 = rf(transforms...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// StoreSecret provides a mock function with given fields: path, secretData +func (_m *ApplicationService) StoreSecret(path string, secretData map[string]string) error { + ret := _m.Called(path, secretData) + + var r0 error + if rf, ok := ret.Get(0).(func(string, map[string]string) error); ok { + r0 = rf(path, secretData) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/interfaces/mocks/BackgroundPublisher.go b/pkg/interfaces/mocks/BackgroundPublisher.go new file mode 100644 index 000000000..f99bb27e9 --- /dev/null +++ b/pkg/interfaces/mocks/BackgroundPublisher.go @@ -0,0 +1,15 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// BackgroundPublisher is an autogenerated mock type for the BackgroundPublisher type +type BackgroundPublisher struct { + mock.Mock +} + +// Publish provides a mock function with given fields: payload, correlationID, contentType +func (_m *BackgroundPublisher) Publish(payload []byte, correlationID string, contentType string) { + _m.Called(payload, correlationID, contentType) +} diff --git a/pkg/interfaces/mocks/Trigger.go b/pkg/interfaces/mocks/Trigger.go new file mode 100644 index 000000000..362b82569 --- /dev/null +++ b/pkg/interfaces/mocks/Trigger.go @@ -0,0 +1,43 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + bootstrap "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" + + mock "github.com/stretchr/testify/mock" + + sync "sync" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// Trigger is an autogenerated mock type for the Trigger type +type Trigger struct { + mock.Mock +} + +// Initialize provides a mock function with given fields: wg, ctx, background +func (_m *Trigger) Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { + ret := _m.Called(wg, ctx, background) + + var r0 bootstrap.Deferred + if rf, ok := ret.Get(0).(func(*sync.WaitGroup, context.Context, <-chan types.MessageEnvelope) bootstrap.Deferred); ok { + r0 = rf(wg, ctx, background) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(bootstrap.Deferred) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*sync.WaitGroup, context.Context, <-chan types.MessageEnvelope) error); ok { + r1 = rf(wg, ctx, background) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/interfaces/mocks/TriggerContextBuilder.go b/pkg/interfaces/mocks/TriggerContextBuilder.go new file mode 100644 index 000000000..b136b826d --- /dev/null +++ b/pkg/interfaces/mocks/TriggerContextBuilder.go @@ -0,0 +1,31 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + mock "github.com/stretchr/testify/mock" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// TriggerContextBuilder is an autogenerated mock type for the TriggerContextBuilder type +type TriggerContextBuilder struct { + mock.Mock +} + +// Execute provides a mock function with given fields: env +func (_m *TriggerContextBuilder) Execute(env types.MessageEnvelope) interfaces.AppFunctionContext { + ret := _m.Called(env) + + var r0 interfaces.AppFunctionContext + if rf, ok := ret.Get(0).(func(types.MessageEnvelope) interfaces.AppFunctionContext); ok { + r0 = rf(env) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interfaces.AppFunctionContext) + } + } + + return r0 +} diff --git a/pkg/interfaces/mocks/TriggerMessageProcessor.go b/pkg/interfaces/mocks/TriggerMessageProcessor.go new file mode 100644 index 000000000..d758ee2e1 --- /dev/null +++ b/pkg/interfaces/mocks/TriggerMessageProcessor.go @@ -0,0 +1,29 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + interfaces "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + mock "github.com/stretchr/testify/mock" + + types "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" +) + +// TriggerMessageProcessor is an autogenerated mock type for the TriggerMessageProcessor type +type TriggerMessageProcessor struct { + mock.Mock +} + +// Execute provides a mock function with given fields: ctx, envelope +func (_m *TriggerMessageProcessor) Execute(ctx interfaces.AppFunctionContext, envelope types.MessageEnvelope) error { + ret := _m.Called(ctx, envelope) + + var r0 error + if rf, ok := ret.Get(0).(func(interfaces.AppFunctionContext, types.MessageEnvelope) error); ok { + r0 = rf(ctx, envelope) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/interfaces/service.go b/pkg/interfaces/service.go new file mode 100644 index 000000000..475533111 --- /dev/null +++ b/pkg/interfaces/service.go @@ -0,0 +1,102 @@ +// +// Copyright (c) 2021 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 interfaces + +import ( + "net/http" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/command" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/notifications" + "github.com/edgexfoundry/go-mod-registry/v2/registry" +) + +const ( + // AppServiceContextKey is the context key for getting the reference to the ApplicationService from the context passed to + // a custom REST Handler + AppServiceContextKey = "AppService" + + // ProfileSuffixPlaceholder is the placeholder text to use in an application service's service key if the + // the name of the configuration profile used is to be used in the service's service key. + // Only useful if the service has multiple configuration profiles to choose from at runtime. + // Example: + // const ( + // serviceKey = "MyServiceName-" + interfaces.ProfileSuffixPlaceholder + // ) + ProfileSuffixPlaceholder = "" +) + +// ApplicationService defines the interface for an edgex Application Service +type ApplicationService interface { + // AddRoute a custom REST route to the application service's internal webserver + // A reference to this ApplicationService is add the the context that is passed to the handler, which + // can be retrieved using the `AppService` key + AddRoute(route string, handler func(http.ResponseWriter, *http.Request), methods ...string) error + // ApplicationSettings returns the key/value map of custom settings + ApplicationSettings() map[string]string + // GetAppSettingStrings is a convenience function that parses the value for the specified custom + // application setting as a comma separated list. It returns the list of strings. + // An error is returned if the specified setting is no found. + GetAppSettingStrings(setting string) ([]string, error) + // SetFunctionsPipeline set the functions pipeline with the specified list of Application Functions. + // Note that the functions are executed in the order provided in the list. + // An error is returned if the list is empty. + SetFunctionsPipeline(transforms ...AppFunction) error + // MakeItRun starts the configured trigger to allow the functions pipeline to execute when the trigger + // receives data and starts the internal webserver. This is a long running function which does not return until + // the service is stopped or MakeItStop() is called. + // An error is returned if the trigger can not be create or initialized or if the internal webserver + // encounters an error. + MakeItRun() error + // MakeItStop stops the configured trigger so that the functions pipeline no longer executes. + // An error is returned + MakeItStop() + // RegisterCustomTriggerFactory registers a trigger factory for a custom trigger to be used. + RegisterCustomTriggerFactory(name string, factory func(TriggerConfig) (Trigger, error)) error + // Adds and returns a BackgroundPublisher which is used to publish asynchronously to the Edgex MessageBus. + // Not valid for use with the HTTP or External MQTT triggers + AddBackgroundPublisher(capacity int) BackgroundPublisher + // GetSecret returns the secret data from the secret store (secure or insecure) for the specified path. + // An error is returned if the path is not found or any of the keys (if specified) are not found. + // Omit keys if all secret data for the specified path is required. + GetSecret(path string, keys ...string) (map[string]string, error) + // StoreSecret stores the specified secret data into the secret store (secure only) for the specified path + // An error is returned if: + // - Specified secret data is empty + // - Not using the secure secret store, i.e. not valid with InsecureSecrets configuration + // - Secure secret provider is not properly initialized + // - Connection issues with Secret Store service. + StoreSecret(path string, secretData map[string]string) error // LoggingClient returns the Logger client + LoggingClient() logger.LoggingClient + // EventClient returns the Event client. Note if Core Data is not specified in the Clients configuration, + // this will return nil. + EventClient() coredata.EventClient + // CommandClient returns the Command client. Note if Support Command is not specified in the Clients configuration, + // this will return nil. + CommandClient() command.CommandClient + // NotificationsClient returns the Notifications client. Note if Support Notifications is not specified in the + // Clients configuration, this will return nil. + NotificationsClient() notifications.NotificationsClient + // RegistryClient() returns the Registry client. Note the registry must been enable, otherwise this will return nil. + // Useful if service needs to add additional health checks or needs to get endpoint of another registered service + RegistryClient() registry.Client + // LoadConfigurablePipeline loads the function pipeline from configuration. + // An error is returned if the configuration is not valid, i.e. missing required function parameters, + // invalid function name, etc. + // Only useful if pipeline from configuration is always defined in configuration as in App Service Configurable. + LoadConfigurablePipeline() ([]AppFunction, error) +} diff --git a/appsdk/trigger.go b/pkg/interfaces/trigger.go similarity index 64% rename from appsdk/trigger.go rename to pkg/interfaces/trigger.go index a3819300a..f6e2a7653 100644 --- a/appsdk/trigger.go +++ b/pkg/interfaces/trigger.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,36 +14,33 @@ // limitations under the License. // -package appsdk +package interfaces import ( "context" "sync" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-messaging/v2/pkg/types" ) -// Trigger interface provides an abstract means to pass messages through the function pipeline +// TriggerConfig provides a container to pass context needed for user defined triggers +type TriggerConfig struct { + Config types.MessageBusConfig + Logger logger.LoggingClient + ContextBuilder TriggerContextBuilder + MessageProcessor TriggerMessageProcessor +} + +// Trigger provides an abstract means to pass messages to the function pipeline type Trigger interface { // Initialize performs post creation initializations Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) } // TriggerMessageProcessor provides an interface that can be used by custom triggers to invoke the runtime -type TriggerMessageProcessor func(ctx *appcontext.Context, envelope types.MessageEnvelope) error +type TriggerMessageProcessor func(ctx AppFunctionContext, envelope types.MessageEnvelope) error -// TriggerContextBuilder provides an interface to construct an appcontext.Context for message -type TriggerContextBuilder func(env types.MessageEnvelope) *appcontext.Context - -// TriggerConfig provides a container to pass context needed to user defined triggers -type TriggerConfig struct { - Config *common.ConfigurationStruct - Logger logger.LoggingClient - ContextBuilder TriggerContextBuilder - MessageProcessor TriggerMessageProcessor -} +// TriggerContextBuilder provides an interface to construct an AppFunctionContext for message +type TriggerContextBuilder func(env types.MessageEnvelope) AppFunctionContext diff --git a/pkg/secure/mqttfactory.go b/pkg/secure/mqttfactory.go index 52e7db352..2c74de40a 100644 --- a/pkg/secure/mqttfactory.go +++ b/pkg/secure/mqttfactory.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,8 +22,9 @@ import ( "errors" "github.com/eclipse/paho.mqtt.golang" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) type mqttSecrets struct { @@ -48,18 +49,17 @@ const ( ) type MqttFactory struct { + appContext interfaces.AppFunctionContext logger logger.LoggingClient - secretProvider interfaces.SecretProvider authMode string secretPath string opts *mqtt.ClientOptions skipCertVerify bool } -func NewMqttFactory(lc logger.LoggingClient, sp interfaces.SecretProvider, mode string, path string, skipVerify bool) MqttFactory { +func NewMqttFactory(appContext interfaces.AppFunctionContext, mode string, path string, skipVerify bool) MqttFactory { return MqttFactory{ - logger: lc, - secretProvider: sp, + appContext: appContext, authMode: mode, secretPath: path, skipCertVerify: skipVerify, @@ -101,7 +101,7 @@ func (factory MqttFactory) getSecrets() (*mqttSecrets, error) { return nil, nil } - secrets, err := factory.secretProvider.GetSecrets(factory.secretPath) + secrets, err := factory.appContext.GetSecret(factory.secretPath) if err != nil { return nil, err } diff --git a/pkg/secure/mqttfactory_test.go b/pkg/secure/mqttfactory_test.go index 06fad0c41..dadc68ee4 100644 --- a/pkg/secure/mqttfactory_test.go +++ b/pkg/secure/mqttfactory_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,13 +21,18 @@ package secure import ( "errors" + "os" "testing" "github.com/eclipse/paho.mqtt.golang" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" ) const testCACert = `-----BEGIN CERTIFICATE----- @@ -102,8 +107,25 @@ CR6KVnoNdMwJZM3ARpBYNlhFTzDyew2WYLitZsN/uV8t+XxJFDyJQA== -----END RSA PRIVATE KEY----- ` +var lc logger.LoggingClient +var dic *di.Container +var context *appfunction.Context + +func TestMain(m *testing.M) { + lc = logger.NewMockClient() + dic = di.NewContainer(di.ServiceConstructorMap{ + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + }) + + context = appfunction.NewContext("123", dic, "") + + os.Exit(m.Run()) +} + func TestValidateSecrets(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) tests := []struct { Name string AuthMode string @@ -162,12 +184,19 @@ func TestGetSecrets(t *testing.T) { "username": "TEST_USER", "password": "TEST_PASS", } + mockSecretProvider := &mocks.SecretProvider{} mockSecretProvider.On("GetSecrets", "").Return(nil) mockSecretProvider.On("GetSecrets", "/notfound").Return(nil, errors.New("Not Found")) mockSecretProvider.On("GetSecrets", "/mqtt").Return(expectedMqttSecrets, nil) - target := NewMqttFactory(logger.NewMockClient(), mockSecretProvider, "", "", false) + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSecretProvider + }, + }) + + target := NewMqttFactory(context, "", "", false) tests := []struct { Name string AuthMode string @@ -201,7 +230,7 @@ func TestGetSecrets(t *testing.T) { } func TestConfigureMQTTClientForAuth(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() tests := []struct { Name string @@ -229,7 +258,7 @@ func TestConfigureMQTTClientForAuth(t *testing.T) { } } func TestConfigureMQTTClientForAuthWithUsernamePassword(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeUsernamePassword err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -244,7 +273,7 @@ func TestConfigureMQTTClientForAuthWithUsernamePassword(t *testing.T) { } func TestConfigureMQTTClientForAuthWithUsernamePasswordAndCA(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeUsernamePassword err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -260,7 +289,7 @@ func TestConfigureMQTTClientForAuthWithUsernamePasswordAndCA(t *testing.T) { } func TestConfigureMQTTClientForAuthWithCACert(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCA err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -276,7 +305,7 @@ func TestConfigureMQTTClientForAuthWithCACert(t *testing.T) { assert.Nil(t, target.opts.TLSConfig.Certificates) } func TestConfigureMQTTClientForAuthWithClientCert(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCert err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -294,7 +323,7 @@ func TestConfigureMQTTClientForAuthWithClientCert(t *testing.T) { } func TestConfigureMQTTClientForAuthWithClientCertNoCA(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeCert err := target.configureMQTTClientForAuth(mqttSecrets{ @@ -311,7 +340,7 @@ func TestConfigureMQTTClientForAuthWithClientCertNoCA(t *testing.T) { assert.Nil(t, target.opts.TLSConfig.ClientCAs) } func TestConfigureMQTTClientForAuthWithNone(t *testing.T) { - target := NewMqttFactory(logger.NewMockClient(), nil, "", "", false) + target := NewMqttFactory(context, "", "", false) target.opts = mqtt.NewClientOptions() target.authMode = AuthModeNone err := target.configureMQTTClientForAuth(mqttSecrets{}) diff --git a/pkg/transforms/batch.go b/pkg/transforms/batch.go index 0f779596f..0434ff80a 100644 --- a/pkg/transforms/batch.go +++ b/pkg/transforms/batch.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,8 +21,8 @@ import ( "sync" "time" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -75,7 +75,7 @@ type BatchConfig struct { batchThreshold int batchMode BatchMode batchData atomicBatchData - continuedPipelineTransforms []appcontext.AppFunction + continuedPipelineTransforms []interfaces.AppFunction timerActive common.AtomicBool done chan bool } @@ -124,19 +124,19 @@ func NewBatchByTimeAndCount(timeInterval string, batchThreshold int) (*BatchConf } // Batch ... -func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (batch *BatchConfig) Batch(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Batching Data") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Batching Data") + byteData, err := util.CoerceType(data) if err != nil { return false, err } // always append data - batch.batchData.append(data) + batch.batchData.append(byteData) // If its time only or time and count if batch.batchMode != BatchByCountOnly { @@ -145,9 +145,9 @@ func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...inte for { select { case <-batch.done: - edgexcontext.LoggingClient.Debug("Batch count has been reached") + ctx.LoggingClient().Debug("Batch count has been reached") case <-time.After(batch.parsedDuration): - edgexcontext.LoggingClient.Debug("Timer has elapsed") + ctx.LoggingClient().Debug("Timer has elapsed") } break } @@ -174,12 +174,12 @@ func (batch *BatchConfig) Batch(edgexcontext *appcontext.Context, params ...inte } } - edgexcontext.LoggingClient.Debug("Forwarding Batched Data...") + ctx.LoggingClient().Debug("Forwarding Batched Data...") // we've met the threshold, lets clear out the buffer and send it forward in the pipeline if batch.batchData.length() > 0 { - copy := batch.batchData.all() + copyOfData := batch.batchData.all() batch.batchData.removeAll() - return true, copy + return true, copyOfData } return false, nil } diff --git a/pkg/transforms/batch_test.go b/pkg/transforms/batch_test.go index 2bd81818d..2c5a7478e 100644 --- a/pkg/transforms/batch_test.go +++ b/pkg/transforms/batch_test.go @@ -1,3 +1,19 @@ +// +// Copyright (c) 2021 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 transforms import ( @@ -13,7 +29,7 @@ var dataToBatch = [3]string{"Test1", "Test2", "Test3"} func TestBatchNoData(t *testing.T) { bs, _ := NewBatchByCount(1) - continuePipeline, err := bs.Batch(context) + continuePipeline, err := bs.Batch(context, nil) assert.False(t, continuePipeline) assert.Equal(t, "No Data Received", err.(error).Error()) diff --git a/pkg/transforms/compression.go b/pkg/transforms/compression.go index 6494b7fd6..8099c096f 100644 --- a/pkg/transforms/compression.go +++ b/pkg/transforms/compression.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,9 +23,11 @@ import ( "compress/zlib" "encoding/base64" "errors" + "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" ) @@ -39,15 +41,15 @@ func NewCompression() Compression { return Compression{} } -// CompressWithGZIP compresses data received as either a string,[]byte, or json.Marshaler using gzip algorithm +// CompressWithGZIP compresses data received as either a string,[]byte, or json.Marshaller using gzip algorithm // and returns a base64 encoded string as a []byte. -func (compression *Compression) CompressWithGZIP(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (compression *Compression) CompressWithGZIP(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Compression with GZIP") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Compression with GZIP") + rawData, err := util.CoerceType(data) if err != nil { return false, err } @@ -59,25 +61,32 @@ func (compression *Compression) CompressWithGZIP(edgexcontext *appcontext.Contex compression.gzipWriter.Reset(&buf) } - compression.gzipWriter.Write([]byte(data)) - compression.gzipWriter.Close() + _, err = compression.gzipWriter.Write(rawData) + if err != nil { + return false, fmt.Errorf("unable to write GZIP data") + } + + err = compression.gzipWriter.Close() + if err != nil { + return false, fmt.Errorf("unable to close GZIP data") + } // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, bytesBufferToBase64(buf) } -// CompressWithZLIB compresses data received as either a string,[]byte, or json.Marshaler using zlib algorithm +// CompressWithZLIB compresses data received as either a string,[]byte, or json.Marshaller using zlib algorithm // and returns a base64 encoded string as a []byte. -func (compression *Compression) CompressWithZLIB(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (compression *Compression) CompressWithZLIB(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - edgexcontext.LoggingClient.Debug("Compression with ZLIB") - data, err := util.CoerceType(params[0]) + ctx.LoggingClient().Debug("Compression with ZLIB") + byteData, err := util.CoerceType(data) if err != nil { return false, err } @@ -89,11 +98,18 @@ func (compression *Compression) CompressWithZLIB(edgexcontext *appcontext.Contex compression.zlibWriter.Reset(&buf) } - compression.zlibWriter.Write([]byte(data)) - compression.zlibWriter.Close() + _, err = compression.zlibWriter.Write(byteData) + if err != nil { + return false, fmt.Errorf("unable to write ZLIB data") + } + + err = compression.zlibWriter.Close() + if err != nil { + return false, fmt.Errorf("unable to close ZLIB data") + } // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, bytesBufferToBase64(buf) diff --git a/pkg/transforms/compression_test.go b/pkg/transforms/compression_test.go index 6010d8895..941da2657 100644 --- a/pkg/transforms/compression_test.go +++ b/pkg/transforms/compression_test.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,8 +33,6 @@ import ( const ( clearString = "This is the test string used for testing" - gzipString = "H4sIAAAJbogA/wrJyCxWyCxWKMlIVShJLS5RKC4pysxLVygtTk1RSMsvAgtm5qUDAgAA//8tdaMdKAAAAA==" - zlibString = "eJwKycgsVsgsVijJSFUoSS0uUSguKcrMS1coLU5NUUjLLwILZualAwIAAP//KucO4w==" ) func TestGzip(t *testing.T) { @@ -59,7 +57,7 @@ func TestGzip(t *testing.T) { continuePipeline2, result2 := comp.CompressWithGZIP(context, []byte(clearString)) assert.True(t, continuePipeline2) assert.Equal(t, result.([]byte), result2.([]byte)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestZlib(t *testing.T) { @@ -85,7 +83,7 @@ func TestZlib(t *testing.T) { continuePipeline2, result2 := comp.CompressWithZLIB(context, []byte(clearString)) assert.True(t, continuePipeline2) assert.Equal(t, result.([]byte), result2.([]byte)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } var result []byte diff --git a/pkg/transforms/conversion.go b/pkg/transforms/conversion.go index 1aef2923e..2294c1c2d 100755 --- a/pkg/transforms/conversion.go +++ b/pkg/transforms/conversion.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import ( "errors" "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) // Conversion houses various built in conversion transforms (XML, JSON, CSV) @@ -38,35 +38,38 @@ func NewConversion() Conversion { // TransformToXML transforms an EdgeX event to XML. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Conversion) TransformToXML(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, stringType interface{}) { - if len(params) < 1 { +func (f Conversion) TransformToXML(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, stringType interface{}) { + if data == nil { return false, errors.New("No Event Received") } - edgexcontext.LoggingClient.Debug("Transforming to XML") - if event, ok := params[0].(dtos.Event); ok { + + ctx.LoggingClient().Debug("Transforming to XML") + if event, ok := data.(dtos.Event); ok { xml, err := event.ToXML() if err != nil { return false, fmt.Errorf("unable to marshal Event to XML: %s", err.Error()) } - edgexcontext.ResponseContentType = clients.ContentTypeXML + + ctx.SetResponseContentType(clients.ContentTypeXML) return true, xml } + return false, errors.New("Unexpected type received") } // TransformToJSON transforms an EdgeX event to JSON. // It will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Conversion) TransformToJSON(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, stringType interface{}) { - if len(params) < 1 { +func (f Conversion) TransformToJSON(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, stringType interface{}) { + if data == nil { return false, errors.New("No Event Received") } - edgexcontext.LoggingClient.Debug("Transforming to JSON") - if result, ok := params[0].(dtos.Event); ok { + ctx.LoggingClient().Debug("Transforming to JSON") + if result, ok := data.(dtos.Event); ok { b, err := json.Marshal(result) if err != nil { return false, errors.New("Error marshalling JSON") } - edgexcontext.ResponseContentType = clients.ContentTypeJSON + ctx.SetResponseContentType(clients.ContentTypeJSON) // should we return a byte[] or string? // return b return true, string(b) diff --git a/pkg/transforms/conversion_test.go b/pkg/transforms/conversion_test.go index 1a89a42d1..592c54867 100644 --- a/pkg/transforms/conversion_test.go +++ b/pkg/transforms/conversion_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -37,16 +37,18 @@ func TestTransformToXML(t *testing.T) { assert.NotNil(t, result) assert.True(t, continuePipeline) - assert.Equal(t, clients.ContentTypeXML, context.ResponseContentType) + assert.Equal(t, clients.ContentTypeXML, context.ResponseContentType()) assert.Equal(t, expectedResult, result.(string)) } -func TestTransformToXMLNoParameters(t *testing.T) { + +func TestTransformToXMLNoData(t *testing.T) { conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context) + continuePipeline, result := conv.TransformToXML(context, nil) assert.Equal(t, "No Event Received", result.(error).Error()) assert.False(t, continuePipeline) } + func TestTransformToXMLNotAnEvent(t *testing.T) { conv := NewConversion() continuePipeline, result := conv.TransformToXML(context, "") @@ -54,36 +56,6 @@ func TestTransformToXMLNotAnEvent(t *testing.T) { assert.Equal(t, "Unexpected type received", result.(error).Error()) assert.False(t, continuePipeline) -} -func TestTransformToXMLMultipleParametersValid(t *testing.T) { - // Event from device 1 - eventIn := dtos.Event{ - DeviceName: deviceName1, - } - expectedResult := `device100` - conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context, eventIn, "", "", "") - require.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) -} -func TestTransformToXMLMultipleParametersTwoEvents(t *testing.T) { - // Event from device 1 - eventIn1 := dtos.Event{ - DeviceName: deviceName1, - } - // Event from device 1 - eventIn2 := dtos.Event{ - DeviceName: deviceName2, - } - expectedResult := `device200` - conv := NewConversion() - continuePipeline, result := conv.TransformToXML(context, eventIn2, eventIn1, "", "") - - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - } func TestTransformToJSON(t *testing.T) { @@ -97,52 +69,22 @@ func TestTransformToJSON(t *testing.T) { assert.NotNil(t, result) assert.True(t, continuePipeline) - assert.Equal(t, clients.ContentTypeJSON, context.ResponseContentType) + assert.Equal(t, clients.ContentTypeJSON, context.ResponseContentType()) assert.Equal(t, expectedResult, result.(string)) } + func TestTransformToJSONNoEvent(t *testing.T) { conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context) + continuePipeline, result := conv.TransformToJSON(context, nil) assert.Equal(t, "No Event Received", result.(error).Error()) assert.False(t, continuePipeline) } + func TestTransformToJSONNotAnEvent(t *testing.T) { conv := NewConversion() continuePipeline, result := conv.TransformToJSON(context, "") require.EqualError(t, result.(error), "Unexpected type received") assert.False(t, continuePipeline) - -} -func TestTransformToJSONMultipleParametersValid(t *testing.T) { - // Event from device 1 - eventIn := dtos.Event{ - DeviceName: deviceName1, - } - expectedResult := `{"apiVersion":"","id":"","deviceName":"device1","profileName":"","sourceName":"","origin":0,"readings":null}` - conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context, eventIn, "", "", "") - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - -} -func TestTransformToJSONMultipleParametersTwoEvents(t *testing.T) { - // Event from device 1 - eventIn1 := dtos.Event{ - DeviceName: deviceName1, - } - // Event from device 2 - eventIn2 := dtos.Event{ - DeviceName: deviceName2, - } - expectedResult := `{"apiVersion":"","id":"","deviceName":"device2","profileName":"","sourceName":"","origin":0,"readings":null}` - conv := NewConversion() - continuePipeline, result := conv.TransformToJSON(context, eventIn2, eventIn1, "", "") - - assert.NotNil(t, result) - assert.True(t, continuePipeline) - assert.Equal(t, expectedResult, result.(string)) - } diff --git a/pkg/transforms/coredata.go b/pkg/transforms/coredata.go index 2ec52ea7a..6cc3b2744 100644 --- a/pkg/transforms/coredata.go +++ b/pkg/transforms/coredata.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,7 +18,7 @@ package transforms import ( "errors" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -29,24 +29,26 @@ type CoreData struct { // NewCoreData Is provided to interact with CoreData func NewCoreData() *CoreData { - coredata := &CoreData{} - return coredata + coreData := &CoreData{} + return coreData } // PushToCoreData pushes the provided value as an event to CoreData using the device name and reading name that have been set. If validation is turned on in // CoreServices then your deviceName and readingName must exist in the CoreMetadata and be properly registered in EdgeX. -func (cdc *CoreData) PushToCoreData(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { - // We didn't receive a result +func (cdc *CoreData) PushToCoreData(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { return false, errors.New("No Data Received") } - val, err := util.CoerceType(params[0]) + + val, err := util.CoerceType(data) if err != nil { return false, err } - result, err := edgexcontext.PushToCoreData(cdc.DeviceName, cdc.ReadingName, val) + + result, err := ctx.PushToCoreData(cdc.DeviceName, cdc.ReadingName, val) if err != nil { return false, err } + return true, result } diff --git a/pkg/transforms/coredata_test.go b/pkg/transforms/coredata_test.go index b6db6812f..bc69a7b3f 100644 --- a/pkg/transforms/coredata_test.go +++ b/pkg/transforms/coredata_test.go @@ -30,11 +30,12 @@ func TestPushToCore_ShouldFailPipelineOnError(t *testing.T) { assert.NotNil(t, result) assert.False(t, continuePipeline) } + func TestPushToCore_NoData(t *testing.T) { coreData := NewCoreData() coreData.DeviceName = "my-device" coreData.ReadingName = "my-device-resource" - continuePipeline, result := coreData.PushToCoreData(context) + continuePipeline, result := coreData.PushToCoreData(context, nil) assert.NotNil(t, result) assert.Equal(t, "No Data Received", result.(error).Error()) diff --git a/pkg/transforms/encryption.go b/pkg/transforms/encryption.go index 36a38ab9c..76774183e 100644 --- a/pkg/transforms/encryption.go +++ b/pkg/transforms/encryption.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -68,12 +68,14 @@ func pkcs5Padding(ciphertext []byte, blockSize int) []byte { // EncryptWithAES encrypts a string, []byte, or json.Marshaller type using AES encryption. // It will return a Base64 encode []byte of the encrypted data. -func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (aesData Encryption) EncryptWithAES(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { return false, errors.New("no data received to encrypt") } - edgexcontext.LoggingClient.Debug("Encrypting with AES") - data, err := util.CoerceType(params[0]) + + ctx.LoggingClient().Debug("Encrypting with AES") + + byteData, err := util.CoerceType(data) if err != nil { return false, err } @@ -87,7 +89,7 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param if len(aesData.SecretPath) != 0 && len(aesData.SecretName) != 0 { // Note secrets are cached so this call doesn't result in unneeded calls to SecretStore Service and // the cache is invalidated when StoreSecrets is used. - secretData, err := edgexcontext.SecretProvider.GetSecrets(aesData.SecretPath, aesData.SecretName) + secretData, err := ctx.GetSecret(aesData.SecretPath, aesData.SecretName) if err != nil { return false, fmt.Errorf( "unable to retieve encryption key at secret path=%s and name=%s", @@ -100,7 +102,7 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param return false, fmt.Errorf("unable find encryption key in secret data for name=%s", aesData.SecretName) } - edgexcontext.LoggingClient.Debugf( + ctx.LoggingClient().Debugf( "Using encryption key from Secret Store at path=%s & name=%s", aesData.SecretPath, aesData.SecretName) @@ -122,14 +124,14 @@ func (aesData Encryption) EncryptWithAES(edgexcontext *appcontext.Context, param } ecb := cipher.NewCBCEncrypter(block, iv) - content := pkcs5Padding(data, block.BlockSize()) + content := pkcs5Padding(byteData, block.BlockSize()) encrypted := make([]byte, len(content)) ecb.CryptBlocks(encrypted, content) encodedData := []byte(base64.StdEncoding.EncodeToString(encrypted)) // Set response "content-type" header to "text/plain" - edgexcontext.ResponseContentType = clients.ContentTypeText + ctx.SetResponseContentType(clients.ContentTypeText) return true, encodedData } diff --git a/pkg/transforms/encryption_test.go b/pkg/transforms/encryption_test.go index a1cfdba03..ba331a959 100644 --- a/pkg/transforms/encryption_test.go +++ b/pkg/transforms/encryption_test.go @@ -1,6 +1,6 @@ // // Copyright (c) 2017 Cavium -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,9 @@ import ( "encoding/base64" "testing" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" "github.com/edgexfoundry/go-mod-core-contracts/v2/models" "github.com/stretchr/testify/assert" @@ -82,7 +84,7 @@ func TestNewEncryption(t *testing.T) { decrypted := aesDecrypt(encrypted.([]byte), aesData) assert.Equal(t, plainString, string(decrypted)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestNewEncryptionWithSecrets(t *testing.T) { @@ -91,7 +93,12 @@ func TestNewEncryptionWithSecrets(t *testing.T) { mockSP := &mocks.SecretProvider{} mockSP.On("GetSecrets", secretPath, secretName).Return(map[string]string{secretName: key}, nil) - context.SecretProvider = mockSP + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSP + }, + }) enc := NewEncryptionWithSecrets(secretPath, secretName, aesData.InitVector) @@ -101,7 +108,7 @@ func TestNewEncryptionWithSecrets(t *testing.T) { decrypted := aesDecrypt(encrypted.([]byte), aesData) assert.Equal(t, plainString, string(decrypted)) - assert.Equal(t, context.ResponseContentType, clients.ContentTypeText) + assert.Equal(t, context.ResponseContentType(), clients.ContentTypeText) } func TestAESNoData(t *testing.T) { @@ -113,7 +120,7 @@ func TestAESNoData(t *testing.T) { enc := NewEncryption(aesData.Key, aesData.InitVector) - continuePipeline, result := enc.EncryptWithAES(context) + continuePipeline, result := enc.EncryptWithAES(context, nil) assert.False(t, continuePipeline) assert.Error(t, result.(error), "expect an error") } diff --git a/pkg/transforms/filter.go b/pkg/transforms/filter.go index 330f7d4e4..b31877eca 100755 --- a/pkg/transforms/filter.go +++ b/pkg/transforms/filter.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" ) @@ -47,13 +47,13 @@ func NewFilterOut(filterValues []string) Filter { // FilterByProfileName filters based on the specified Device Profile, aka Class of Device. // If FilterOut is false, it filters out those Events not associated with the specified Device Profile listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Device Profile listed in FilterValues. -func (f Filter) FilterByProfileName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterByProfileName", "ProfileName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByProfileName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterByProfileName", "ProfileName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("ProfileName", event.ProfileName, edgexcontext.LoggingClient) + ok := f.doEventFilter("ProfileName", event.ProfileName, ctx.LoggingClient()) if ok { return true, *event } @@ -65,13 +65,13 @@ func (f Filter) FilterByProfileName(edgexcontext *appcontext.Context, params ... // FilterByDeviceName filters based on the specified Device Names, aka Instance of a Device. // If FilterOut is false, it filters out those Events not associated with the specified Device Names listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Device Names listed in FilterValues. -func (f Filter) FilterByDeviceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterByDeviceName", "DeviceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByDeviceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterByDeviceName", "DeviceName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("DeviceName", event.DeviceName, edgexcontext.LoggingClient) + ok := f.doEventFilter("DeviceName", event.DeviceName, ctx.LoggingClient()) if ok { return true, *event } @@ -82,13 +82,13 @@ func (f Filter) FilterByDeviceName(edgexcontext *appcontext.Context, params ...i // FilterBySourceName filters based on the specified Source for the Event, aka resource or command name. // If FilterOut is false, it filters out those Events not associated with the specified Source listed in FilterValues. // If FilterOut is true, it out those Events that are associated with the specified Source listed in FilterValues. -func (f Filter) FilterBySourceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - event, err := f.setupForFiltering("FilterBySourceName", "SourceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterBySourceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + event, err := f.setupForFiltering("FilterBySourceName", "SourceName", ctx.LoggingClient(), data) if err != nil { return false, err } - ok := f.doEventFilter("SourceName", event.SourceName, edgexcontext.LoggingClient) + ok := f.doEventFilter("SourceName", event.SourceName, ctx.LoggingClient()) if ok { return true, *event } @@ -100,8 +100,8 @@ func (f Filter) FilterBySourceName(edgexcontext *appcontext.Context, params ...i // If FilterOut is false, it filters out those Event Readings not associated with the specified Resource Names listed in FilterValues. // If FilterOut is true, it out those Event Readings that are associated with the specified Resource Names listed in FilterValues. // This function will return an error and stop the pipeline if a non-edgex event is received or if no data is received. -func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params ...interface{}) (continuePipeline bool, result interface{}) { - existingEvent, err := f.setupForFiltering("FilterByResourceName", "ResourceName", edgexcontext.LoggingClient, params...) +func (f Filter) FilterByResourceName(ctx interfaces.AppFunctionContext, data interface{}) (continuePipeline bool, result interface{}) { + existingEvent, err := f.setupForFiltering("FilterByResourceName", "ResourceName", ctx.LoggingClient(), data) if err != nil { return false, err } @@ -129,10 +129,10 @@ func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params .. } if !readingFilteredOut { - edgexcontext.LoggingClient.Debugf("Reading accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading accepted: %s", reading.ResourceName) auxEvent.Readings = append(auxEvent.Readings, reading) } else { - edgexcontext.LoggingClient.Debugf("Reading not accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading not accepted: %s", reading.ResourceName) } } } else { @@ -146,35 +146,35 @@ func (f Filter) FilterByResourceName(edgexcontext *appcontext.Context, params .. } if readingFilteredFor { - edgexcontext.LoggingClient.Debugf("Reading accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading accepted: %s", reading.ResourceName) auxEvent.Readings = append(auxEvent.Readings, reading) } else { - edgexcontext.LoggingClient.Debugf("Reading not accepted: %s", reading.ResourceName) + ctx.LoggingClient().Debugf("Reading not accepted: %s", reading.ResourceName) } } } if len(auxEvent.Readings) > 0 { - edgexcontext.LoggingClient.Debugf("Event accepted: %d remaining reading(s)", len(auxEvent.Readings)) + ctx.LoggingClient().Debugf("Event accepted: %d remaining reading(s)", len(auxEvent.Readings)) return true, auxEvent } - edgexcontext.LoggingClient.Debug("Event not accepted: 0 remaining readings") + ctx.LoggingClient().Debug("Event not accepted: 0 remaining readings") return false, nil } -func (f Filter) setupForFiltering(funcName string, filterProperty string, lc logger.LoggingClient, params ...interface{}) (*dtos.Event, error) { +func (f Filter) setupForFiltering(funcName string, filterProperty string, lc logger.LoggingClient, data interface{}) (*dtos.Event, error) { mode := "For" if f.FilterOut { mode = "Out" } lc.Debugf("Filtering %s by %s. FilterValues are: '[%v]'", mode, filterProperty, f.FilterValues) - if len(params) < 1 { + if data == nil { return nil, fmt.Errorf("%s: no Event Received", funcName) } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return nil, fmt.Errorf("%s: type received is not an Event", funcName) } diff --git a/pkg/transforms/filter_test.go b/pkg/transforms/filter_test.go index a0270cd74..2cc7d76bb 100644 --- a/pkg/transforms/filter_test.go +++ b/pkg/transforms/filter_test.go @@ -50,19 +50,18 @@ func TestFilter_FilterByProfileName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParam bool }{ - {"filter for - no event", []string{profileName1}, true, nil, true, false}, - {"filter for - no filter values", []string{}, false, &profile1Event, false, false}, - {"filter for with extra params - found", []string{profileName1}, false, &profile1Event, false, true}, - {"filter for - found", []string{profileName1}, false, &profile1Event, false, false}, - {"filter for - not found", []string{profileName2}, false, &profile1Event, true, false}, - - {"filter out - no event", []string{profileName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &profile1Event, false, false}, - {"filter out extra param - found", []string{profileName1}, true, &profile1Event, true, true}, - {"filter out - found", []string{profileName1}, true, &profile1Event, true, false}, - {"filter out - not found", []string{profileName2}, true, &profile1Event, false, false}, + {"filter for - no event", []string{profileName1}, true, nil, true}, + {"filter for - no filter values", []string{}, false, &profile1Event, false}, + {"filter for with extra data - found", []string{profileName1}, false, &profile1Event, false}, + {"filter for - found", []string{profileName1}, false, &profile1Event, false}, + {"filter for - not found", []string{profileName2}, false, &profile1Event, true}, + + {"filter out - no event", []string{profileName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &profile1Event, false}, + {"filter out extra param - found", []string{profileName1}, true, &profile1Event, true}, + {"filter out - found", []string{profileName1}, true, &profile1Event, true}, + {"filter out - not found", []string{profileName2}, true, &profile1Event, false}, } for _, test := range tests { @@ -77,17 +76,11 @@ func TestFilter_FilterByProfileName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByProfileName(context) + continuePipeline, result := filter.FilterByProfileName(context, nil) assert.EqualError(t, result.(error), "FilterByProfileName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParam { - continuePipeline, result = filter.FilterByProfileName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByProfileName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByProfileName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -107,19 +100,18 @@ func TestFilter_FilterByDeviceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParams bool }{ - {"filter for - no event", []string{deviceName1}, false, nil, true, false}, - {"filter for - no filter values", []string{}, false, &device1Event, false, false}, - {"filter for with extra params - found", []string{deviceName1}, false, &device1Event, false, true}, - {"filter for - found", []string{deviceName1}, false, &device1Event, false, false}, - {"filter for - not found", []string{deviceName2}, false, &device1Event, true, false}, - - {"filter out - no event", []string{deviceName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &device1Event, false, false}, - {"filter out extra param - found", []string{deviceName1}, true, &device1Event, true, true}, - {"filter out - found", []string{deviceName1}, true, &device1Event, true, false}, - {"filter out - not found", []string{deviceName2}, true, &device1Event, false, false}, + {"filter for - no event", []string{deviceName1}, false, nil, true}, + {"filter for - no filter values", []string{}, false, &device1Event, false}, + {"filter for with extra data - found", []string{deviceName1}, false, &device1Event, false}, + {"filter for - found", []string{deviceName1}, false, &device1Event, false}, + {"filter for - not found", []string{deviceName2}, false, &device1Event, true}, + + {"filter out - no event", []string{deviceName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &device1Event, false}, + {"filter out extra param - found", []string{deviceName1}, true, &device1Event, true}, + {"filter out - found", []string{deviceName1}, true, &device1Event, true}, + {"filter out - not found", []string{deviceName2}, true, &device1Event, false}, } for _, test := range tests { @@ -134,17 +126,11 @@ func TestFilter_FilterByDeviceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByDeviceName(context) + continuePipeline, result := filter.FilterByDeviceName(context, nil) assert.EqualError(t, result.(error), "FilterByDeviceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParams { - continuePipeline, result = filter.FilterByDeviceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByDeviceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByDeviceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -164,19 +150,18 @@ func TestFilter_FilterBySourceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParam bool }{ - {"filter for - no event", []string{sourceName1}, true, nil, true, false}, - {"filter for - no filter values", []string{}, false, &source1Event, false, false}, - {"filter for with extra params - found", []string{sourceName1}, false, &source1Event, false, true}, - {"filter for - found", []string{sourceName1}, false, &source1Event, false, false}, - {"filter for - not found", []string{sourceName2}, false, &source1Event, true, false}, - - {"filter out - no event", []string{sourceName1}, true, nil, true, false}, - {"filter out - no filter values", []string{}, true, &source1Event, false, false}, - {"filter out extra param - found", []string{sourceName1}, true, &source1Event, true, true}, - {"filter out - found", []string{sourceName1}, true, &source1Event, true, false}, - {"filter out - not found", []string{sourceName2}, true, &source1Event, false, false}, + {"filter for - no event", []string{sourceName1}, true, nil, true}, + {"filter for - no filter values", []string{}, false, &source1Event, false}, + {"filter for with extra data - found", []string{sourceName1}, false, &source1Event, false}, + {"filter for - found", []string{sourceName1}, false, &source1Event, false}, + {"filter for - not found", []string{sourceName2}, false, &source1Event, true}, + + {"filter out - no event", []string{sourceName1}, true, nil, true}, + {"filter out - no filter values", []string{}, true, &source1Event, false}, + {"filter out extra param - found", []string{sourceName1}, true, &source1Event, true}, + {"filter out - found", []string{sourceName1}, true, &source1Event, true}, + {"filter out - not found", []string{sourceName2}, true, &source1Event, false}, } for _, test := range tests { @@ -191,17 +176,11 @@ func TestFilter_FilterBySourceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterBySourceName(context) + continuePipeline, result := filter.FilterBySourceName(context, nil) assert.EqualError(t, result.(error), "FilterBySourceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParam { - continuePipeline, result = filter.FilterBySourceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterBySourceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterBySourceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil && test.EventIn != nil { @@ -241,29 +220,28 @@ func TestFilter_FilterByResourceName(t *testing.T) { FilterOut bool EventIn *dtos.Event ExpectedNilResult bool - ExtraParams bool ExpectedReadingCount int }{ - {"filter for - no event", []string{resource1}, false, nil, true, false, 0}, - {"filter for extra param - found", []string{resource1}, false, &resource1Event, false, true, 1}, - {"filter for 0 in R1 - no change", []string{}, false, &resource1Event, false, false, 1}, - {"filter for 1 in R1 - 1 of 1 found", []string{resource1}, false, &resource1Event, false, false, 1}, - {"filter for 1 in 2R - 1 of 2 found", []string{resource1}, false, &twoResourceEvent, false, false, 1}, - {"filter for 2 in R1 - 1 of 1 found", []string{resource1, resource2}, false, &resource1Event, false, false, 1}, - {"filter for 2 in 2R - 2 of 2 found", []string{resource1, resource2}, false, &twoResourceEvent, false, false, 2}, - {"filter for 2 in R2 - 1 of 2 found", []string{resource1, resource2}, false, &resource2Event, false, false, 1}, - {"filter for 1 in R2 - not found", []string{resource1}, false, &resource2Event, true, false, 0}, - - {"filter out - no event", []string{resource1}, true, nil, true, false, 0}, - {"filter out extra param - found", []string{resource1}, true, &resource1Event, true, true, 0}, - {"filter out 0 in R1 - no change", []string{}, true, &resource1Event, false, false, 1}, - {"filter out 1 in R1 - 1 of 1 found", []string{resource1}, true, &resource1Event, true, false, 0}, - {"filter out 1 in R2 - not found", []string{resource1}, true, &resource2Event, false, false, 1}, - {"filter out 1 in 2R - 1 of 2 found", []string{resource1}, true, &twoResourceEvent, false, false, 1}, - {"filter out 2 in R1 - 1 of 1 found", []string{resource1, resource2}, true, &resource1Event, true, false, 0}, - {"filter out 2 in R2 - 1 of 1 found", []string{resource1, resource2}, true, &resource2Event, true, false, 0}, - {"filter out 2 in 2R - 2 of 2 found", []string{resource1, resource2}, true, &twoResourceEvent, true, false, 0}, - {"filter out 2 in R3 - not found", []string{resource1, resource2}, true, &resource3Event, false, false, 1}, + {"filter for - no event", []string{resource1}, false, nil, true, 0}, + {"filter for extra param - found", []string{resource1}, false, &resource1Event, false, 1}, + {"filter for 0 in R1 - no change", []string{}, false, &resource1Event, false, 1}, + {"filter for 1 in R1 - 1 of 1 found", []string{resource1}, false, &resource1Event, false, 1}, + {"filter for 1 in 2R - 1 of 2 found", []string{resource1}, false, &twoResourceEvent, false, 1}, + {"filter for 2 in R1 - 1 of 1 found", []string{resource1, resource2}, false, &resource1Event, false, 1}, + {"filter for 2 in 2R - 2 of 2 found", []string{resource1, resource2}, false, &twoResourceEvent, false, 2}, + {"filter for 2 in R2 - 1 of 2 found", []string{resource1, resource2}, false, &resource2Event, false, 1}, + {"filter for 1 in R2 - not found", []string{resource1}, false, &resource2Event, true, 0}, + + {"filter out - no event", []string{resource1}, true, nil, true, 0}, + {"filter out extra param - found", []string{resource1}, true, &resource1Event, true, 0}, + {"filter out 0 in R1 - no change", []string{}, true, &resource1Event, false, 1}, + {"filter out 1 in R1 - 1 of 1 found", []string{resource1}, true, &resource1Event, true, 0}, + {"filter out 1 in R2 - not found", []string{resource1}, true, &resource2Event, false, 1}, + {"filter out 1 in 2R - 1 of 2 found", []string{resource1}, true, &twoResourceEvent, false, 1}, + {"filter out 2 in R1 - 1 of 1 found", []string{resource1, resource2}, true, &resource1Event, true, 0}, + {"filter out 2 in R2 - 1 of 1 found", []string{resource1, resource2}, true, &resource2Event, true, 0}, + {"filter out 2 in 2R - 2 of 2 found", []string{resource1, resource2}, true, &twoResourceEvent, true, 0}, + {"filter out 2 in R3 - not found", []string{resource1, resource2}, true, &resource3Event, false, 1}, } for _, test := range tests { @@ -278,17 +256,11 @@ func TestFilter_FilterByResourceName(t *testing.T) { expectedContinue := !test.ExpectedNilResult if test.EventIn == nil { - continuePipeline, result := filter.FilterByResourceName(context) + continuePipeline, result := filter.FilterByResourceName(context, nil) assert.EqualError(t, result.(error), "FilterByResourceName: no Event Received") assert.False(t, continuePipeline) } else { - var continuePipeline bool - var result interface{} - if test.ExtraParams { - continuePipeline, result = filter.FilterByResourceName(context, *test.EventIn, "application/event") - } else { - continuePipeline, result = filter.FilterByResourceName(context, *test.EventIn) - } + continuePipeline, result := filter.FilterByResourceName(context, *test.EventIn) assert.Equal(t, expectedContinue, continuePipeline) assert.Equal(t, test.ExpectedNilResult, result == nil) if result != nil { diff --git a/pkg/transforms/http.go b/pkg/transforms/http.go index 320b37884..5d90f872b 100644 --- a/pkg/transforms/http.go +++ b/pkg/transforms/http.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,7 +23,7 @@ import ( "io/ioutil" "net/http" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" @@ -63,21 +63,21 @@ func NewHTTPSenderWithSecretHeader(url string, mimeType string, persistOnError b // HTTPPost will send data from the previous function to the specified Endpoint via http POST. // If no previous function exists, then the event that triggered the pipeline will be used. // An empty string for the mimetype will default to application/json. -func (sender HTTPSender) HTTPPost(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return sender.httpSend(edgexcontext, params, http.MethodPost) +func (sender HTTPSender) HTTPPost(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return sender.httpSend(ctx, data, http.MethodPost) } // HTTPPut will send data from the previous function to the specified Endpoint via http PUT. // If no previous function exists, then the event that triggered the pipeline will be used. // An empty string for the mimetype will default to application/json. -func (sender HTTPSender) HTTPPut(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - return sender.httpSend(edgexcontext, params, http.MethodPut) +func (sender HTTPSender) HTTPPut(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + return sender.httpSend(ctx, data, http.MethodPut) } -func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []interface{}, method string) (bool, interface{}) { - lc := edgexcontext.LoggingClient +func (sender HTTPSender) httpSend(ctx interfaces.AppFunctionContext, data interface{}, method string) (bool, interface{}) { + lc := ctx.LoggingClient() - if len(params) < 1 { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } @@ -86,7 +86,7 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int sender.MimeType = "application/json" } - exportData, err := util.CoerceType(params[0]) + exportData, err := util.CoerceType(data) if err != nil { return false, err } @@ -103,7 +103,7 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int } var theSecrets map[string]string if usingSecrets { - theSecrets, err = edgexcontext.GetSecrets(sender.SecretPath, sender.SecretName) + theSecrets, err = ctx.GetSecret(sender.SecretPath, sender.SecretName) if err != nil { return false, err } @@ -118,26 +118,26 @@ func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []int req.Header.Set("Content-Type", sender.MimeType) - edgexcontext.LoggingClient.Debug("POSTing data") + ctx.LoggingClient().Debug("POSTing data") response, err := client.Do(req) if err != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, err } defer func() { _ = response.Body.Close() }() - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Response: %s", response.Status)) - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Sent data: %s", string(exportData))) + ctx.LoggingClient().Debug(fmt.Sprintf("Response: %s", response.Status)) + ctx.LoggingClient().Debug(fmt.Sprintf("Sent data: %s", string(exportData))) bodyBytes, errReadingBody := ioutil.ReadAll(response.Body) if errReadingBody != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, errReadingBody } - edgexcontext.LoggingClient.Trace("Data exported", "Transport", "HTTP", clients.CorrelationHeader, edgexcontext.CorrelationID) + ctx.LoggingClient().Trace("Data exported", "Transport", "HTTP", clients.CorrelationHeader, ctx.CorrelationID) // continues the pipeline if we get a 2xx response, stops pipeline if non-2xx response if response.StatusCode < 200 || response.StatusCode >= 300 { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, fmt.Errorf("export failed with %d HTTP status code", response.StatusCode) } @@ -170,8 +170,8 @@ func (sender HTTPSender) determineIfUsingSecrets() (bool, error) { return true, nil } -func (sender HTTPSender) setRetryData(ctx *appcontext.Context, exportData []byte) { +func (sender HTTPSender) setRetryData(ctx interfaces.AppFunctionContext, exportData []byte) { if sender.PersistOnError { - ctx.RetryData = exportData + ctx.SetRetryData(exportData) } } diff --git a/pkg/transforms/http_test.go b/pkg/transforms/http_test.go index 286f59df2..c217a5787 100644 --- a/pkg/transforms/http_test.go +++ b/pkg/transforms/http_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,16 +25,10 @@ import ( "strings" "testing" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + mocks2 "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,28 +39,7 @@ const ( badPath = "/some-path/bad" ) -var lc logger.LoggingClient -var config *common.ConfigurationStruct -var context *appcontext.Context - -func TestMain(m *testing.M) { - lc = logger.NewMockClient() - eventClient := coredata.NewEventClient(local.New("http://test" + clients.ApiEventRoute)) - - config = &common.ConfigurationStruct{} - - context = &appcontext.Context{ - LoggingClient: lc, - EventClient: eventClient, - Configuration: config, - } - - m.Run() -} - func TestHTTPPostPut(t *testing.T) { - context.CorrelationID = "123" - var methodUsed string handler := func(w http.ResponseWriter, r *http.Request) { @@ -118,7 +91,7 @@ func TestHTTPPostPut(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) methodUsed = "" sender := NewHTTPSender(`http://`+targetUrl.Host+test.Path, "", test.PersistOnFail) @@ -128,7 +101,7 @@ func TestHTTPPostPut(t *testing.T) { sender.HTTPPut(context, msgStr) } - assert.Equal(t, test.RetryDataSet, context.RetryData != nil) + assert.Equal(t, test.RetryDataSet, context.RetryData() != nil) assert.Equal(t, test.ExpectedMethod, methodUsed) }) } @@ -139,10 +112,15 @@ func TestHTTPPostPutWithSecrets(t *testing.T) { expectedValue := "my-API-key" - mockSP := &mocks.SecretProvider{} + mockSP := &mocks2.SecretProvider{} mockSP.On("GetSecrets", "/path", "header").Return(map[string]string{"Secret-Header-Name": expectedValue}, nil) mockSP.On("GetSecrets", "/path", "bogus").Return(nil, errors.New("FAKE NOT FOUND ERROR")) - context.SecretProvider = mockSP + + dic.Update(di.ServiceConstructorMap{ + bootstrapContainer.SecretProviderName: func(get di.Get) interface{} { + return mockSP + }, + }) // create test server with handler ts := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { @@ -221,7 +199,7 @@ func TestHTTPPostPutWithSecrets(t *testing.T) { func TestHTTPPostNoParameterPassed(t *testing.T) { sender := NewHTTPSender("", "", false) - continuePipeline, result := sender.HTTPPost(context) + continuePipeline, result := sender.HTTPPost(context, nil) assert.False(t, continuePipeline, "Pipeline should stop") assert.Error(t, result.(error), "Result should be an error") @@ -230,7 +208,7 @@ func TestHTTPPostNoParameterPassed(t *testing.T) { func TestHTTPPutNoParameterPassed(t *testing.T) { sender := NewHTTPSender("", "", false) - continuePipeline, result := sender.HTTPPut(context) + continuePipeline, result := sender.HTTPPut(context, nil) assert.False(t, continuePipeline, "Pipeline should stop") assert.Error(t, result.(error), "Result should be an error") diff --git a/pkg/transforms/jsonlogic.go b/pkg/transforms/jsonlogic.go index ff6844de1..c09538748 100644 --- a/pkg/transforms/jsonlogic.go +++ b/pkg/transforms/jsonlogic.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,11 +20,13 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "strconv" "strings" "github.com/diegoholiveira/jsonlogic" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -41,29 +43,35 @@ func NewJSONLogic(rule string) JSONLogic { } // Evaluate ... -func (logic JSONLogic) Evaluate(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (logic JSONLogic) Evaluate(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - coercedData, err := util.CoerceType(params[0]) + coercedData, err := util.CoerceType(data) if err != nil { return false, err } - data := strings.NewReader(string(coercedData)) + reader := strings.NewReader(string(coercedData)) rule := strings.NewReader(logic.Rule) - var logicresult bytes.Buffer - edgexcontext.LoggingClient.Debug("Applying JSONLogic Rule") - err = jsonlogic.Apply(rule, data, &logicresult) + + var logicResult bytes.Buffer + ctx.LoggingClient().Debug("Applying JSONLogic Rule") + err = jsonlogic.Apply(rule, reader, &logicResult) if err != nil { - return false, err + return false, fmt.Errorf("unable to apply JSONLogic rule: %s", err.Error()) } + var result bool - decoder := json.NewDecoder(&logicresult) - decoder.Decode(&result) - edgexcontext.LoggingClient.Debug("Condition met: " + strconv.FormatBool(result)) + decoder := json.NewDecoder(&logicResult) + err = decoder.Decode(&result) + if err != nil { + return false, fmt.Errorf("unable to decode JSONLogic result: %s", err.Error()) + } + + ctx.LoggingClient().Debug("Condition met: " + strconv.FormatBool(result)) - return result, params[0] + return result, data } diff --git a/pkg/transforms/jsonlogic_test.go b/pkg/transforms/jsonlogic_test.go index 83afe2dad..fd7a5a64c 100644 --- a/pkg/transforms/jsonlogic_test.go +++ b/pkg/transforms/jsonlogic_test.go @@ -1,16 +1,33 @@ +// +// Copyright (c) 2021 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 transforms import ( + "fmt" "testing" - jlogic "github.com/diegoholiveira/jsonlogic" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestJSONLogicSimple(t *testing.T) { - jsonlogic := NewJSONLogic(`{"==": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, "{}") + continuePipeline, result := jsonLogic.Evaluate(context, "{}") assert.NotNil(t, result) assert.True(t, continuePipeline) @@ -18,13 +35,13 @@ func TestJSONLogicSimple(t *testing.T) { } func TestJSONLogicAdvanced(t *testing.T) { - jsonlogic := NewJSONLogic(`{ "and" : [ + jsonLogic := NewJSONLogic(`{ "and" : [ {"<" : [ { "var" : "temp" }, 110 ]}, {"==" : [ { "var" : "sensor.type" }, "temperature" ] } ] }`) data := `{ "temp" : 100, "sensor" : { "type" : "temperature" } }` - continuePipeline, result := jsonlogic.Evaluate(context, data) + continuePipeline, result := jsonLogic.Evaluate(context, data) assert.NotNil(t, result) assert.True(t, continuePipeline) @@ -33,9 +50,9 @@ func TestJSONLogicAdvanced(t *testing.T) { func TestJSONLogicMalformedJSONRule(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"==: [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==: [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, `{}`) + continuePipeline, result := jsonLogic.Evaluate(context, `{}`) assert.NotNil(t, result) assert.False(t, continuePipeline) @@ -44,20 +61,21 @@ func TestJSONLogicMalformedJSONRule(t *testing.T) { func TestJSONLogicValidJSONBadRule(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"notanoperator": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"notAnOperator": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, `{}`) + continuePipeline, result := jsonLogic.Evaluate(context, `{}`) assert.NotNil(t, result) assert.False(t, continuePipeline) - assert.Equal(t, "The operator \"notanoperator\" is not supported", result.(jlogic.ErrInvalidOperator).Error()) + require.IsType(t, fmt.Errorf(""), result) + assert.Equal(t, "unable to apply JSONLogic rule: The operator \"notAnOperator\" is not supported", result.(error).Error()) } func TestJSONLogicNoData(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"notanoperator": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"notAnOperator": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context) + continuePipeline, result := jsonLogic.Evaluate(context, nil) assert.NotNil(t, result) assert.False(t, continuePipeline) @@ -66,9 +84,9 @@ func TestJSONLogicNoData(t *testing.T) { func TestJSONLogicNonJSONData(t *testing.T) { //missing quote - jsonlogic := NewJSONLogic(`{"==": [1, 1]}`) + jsonLogic := NewJSONLogic(`{"==": [1, 1]}`) - continuePipeline, result := jsonlogic.Evaluate(context, "iamnotjson") + continuePipeline, result := jsonLogic.Evaluate(context, "iAmNotJson") assert.NotNil(t, result) assert.False(t, continuePipeline) diff --git a/pkg/transforms/mqttsecret.go b/pkg/transforms/mqttsecret.go index 06f9a7167..7407d4b3f 100644 --- a/pkg/transforms/mqttsecret.go +++ b/pkg/transforms/mqttsecret.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import ( MQTT "github.com/eclipse/paho.mqtt.golang" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/secure" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" ) @@ -83,23 +83,18 @@ func NewMQTTSecretSender(mqttConfig MQTTSecretConfig, persistOnError bool) *MQTT return sender } -func (sender *MQTTSecretSender) initializeMQTTClient(edgexcontext *appcontext.Context) error { +func (sender *MQTTSecretSender) initializeMQTTClient(ctx interfaces.AppFunctionContext) error { sender.lock.Lock() defer sender.lock.Unlock() // If the conditions changed while waiting for the lock, i.e. other thread completed the initialization, // then skip doing anything - if sender.client != nil && !sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.SecretsLastUpdated()) { + if sender.client != nil && !sender.secretsLastRetrieved.Before(ctx.SecretsLastUpdated()) { return nil } - mqttFactory := secure.NewMqttFactory( - edgexcontext.LoggingClient, - edgexcontext.SecretProvider, - sender.mqttConfig.AuthMode, - sender.mqttConfig.SecretPath, - sender.mqttConfig.SkipCertVerify, - ) + config := sender.mqttConfig + mqttFactory := secure.NewMqttFactory(ctx, config.AuthMode, config.SecretPath, config.SkipCertVerify) client, err := mqttFactory.Create(sender.opts) if err != nil { @@ -112,7 +107,7 @@ func (sender *MQTTSecretSender) initializeMQTTClient(edgexcontext *appcontext.Co return nil } -func (sender *MQTTSecretSender) connectToBroker(edgexcontext *appcontext.Context, exportData []byte) error { +func (sender *MQTTSecretSender) connectToBroker(ctx interfaces.AppFunctionContext, exportData []byte) error { sender.lock.Lock() defer sender.lock.Unlock() @@ -122,40 +117,40 @@ func (sender *MQTTSecretSender) connectToBroker(edgexcontext *appcontext.Context return nil } - edgexcontext.LoggingClient.Info("Connecting to mqtt server for export") + ctx.LoggingClient().Info("Connecting to mqtt server for export") if token := sender.client.Connect(); token.Wait() && token.Error() != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) subMessage := "dropping event" if sender.persistOnError { subMessage = "persisting Event for later retry" } return fmt.Errorf("Could not connect to mqtt server for export, %s. Error: %s", subMessage, token.Error().Error()) } - edgexcontext.LoggingClient.Info("Connected to mqtt server for export") + ctx.LoggingClient().Info("Connected to mqtt server for export") return nil } // MQTTSend sends data from the previous function to the specified MQTT broker. // If no previous function exists, then the event that triggered the pipeline will be used. -func (sender *MQTTSecretSender) MQTTSend(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - if len(params) < 1 { +func (sender *MQTTSecretSender) MQTTSend(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { // We didn't receive a result return false, errors.New("No Data Received") } - exportData, err := util.CoerceType(params[0]) + exportData, err := util.CoerceType(data) if err != nil { return false, err } // if we havent initialized the client yet OR the cache has been invalidated (due to new/updated secrets) we need to (re)initialize the client - if sender.client == nil || sender.secretsLastRetrieved.Before(edgexcontext.SecretProvider.SecretsLastUpdated()) { - err := sender.initializeMQTTClient(edgexcontext) + if sender.client == nil || sender.secretsLastRetrieved.Before(ctx.SecretsLastUpdated()) { + err := sender.initializeMQTTClient(ctx) if err != nil { return false, err } } if !sender.client.IsConnected() { - err := sender.connectToBroker(edgexcontext, exportData) + err := sender.connectToBroker(ctx, exportData) if err != nil { return false, err } @@ -164,18 +159,18 @@ func (sender *MQTTSecretSender) MQTTSend(edgexcontext *appcontext.Context, param token := sender.client.Publish(sender.mqttConfig.Topic, sender.mqttConfig.QoS, sender.mqttConfig.Retain, exportData) token.Wait() if token.Error() != nil { - sender.setRetryData(edgexcontext, exportData) + sender.setRetryData(ctx, exportData) return false, token.Error() } - edgexcontext.LoggingClient.Debug("Sent data to MQTT Broker") - edgexcontext.LoggingClient.Trace("Data exported", "Transport", "MQTT", clients.CorrelationHeader, edgexcontext.CorrelationID) + ctx.LoggingClient().Debug("Sent data to MQTT Broker") + ctx.LoggingClient().Trace("Data exported", "Transport", "MQTT", clients.CorrelationHeader, ctx.CorrelationID) return true, nil } -func (sender *MQTTSecretSender) setRetryData(ctx *appcontext.Context, exportData []byte) { +func (sender *MQTTSecretSender) setRetryData(ctx interfaces.AppFunctionContext, exportData []byte) { if sender.persistOnError { - ctx.RetryData = exportData + ctx.SetRetryData(exportData) } } diff --git a/pkg/transforms/mqttsecret_broker_test.go b/pkg/transforms/mqttsecret_broker_test.go deleted file mode 100644 index 45feef653..000000000 --- a/pkg/transforms/mqttsecret_broker_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// +build brokerRunning -// -// 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. -// - -// This test will only be executed if the tag brokerRunning is added when running -// the tests with a command like: -// go test -tags brokerRunning - -package transforms - -import ( - "testing" - "time" - - MQTT "github.com/eclipse/paho.mqtt.golang" - "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/interfaces/mocks" -) - -func TestMQTTSendWithData(t *testing.T) { - sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) - sender.mqttConfig = MQTTSecretConfig{ - SecretPath: "/mqtt", - } - sender.client = MQTT.NewClient(sender.opts) - mockSecretProvider := &mocks.SecretProvider{} - mockSecretProvider.On("SecretsLastUpdated").Return(time.Now()) - context.SecretProvider = mockSecretProvider - sender.MQTTSend(context, "sendme") - // require.True(t, continuePipeline) - // require.Error(t, result.(error)) -} diff --git a/pkg/transforms/mqttsecret_test.go b/pkg/transforms/mqttsecret_test.go index b30b047fb..952a48f83 100644 --- a/pkg/transforms/mqttsecret_test.go +++ b/pkg/transforms/mqttsecret_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,25 +27,25 @@ import ( ) func TestSetRetryDataPersistFalse(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) sender := NewMQTTSecretSender(MQTTSecretConfig{}, false) sender.mqttConfig = MQTTSecretConfig{} sender.setRetryData(context, []byte("data")) - assert.Nil(t, context.RetryData) + assert.Nil(t, context.RetryData()) } func TestSetRetryDataPersistTrue(t *testing.T) { - context.RetryData = nil + context.SetRetryData(nil) sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) sender.mqttConfig = MQTTSecretConfig{} sender.setRetryData(context, []byte("data")) - assert.Equal(t, []byte("data"), context.RetryData) + assert.Equal(t, []byte("data"), context.RetryData()) } -func TestMQTTSendNoParams(t *testing.T) { +func TestMQTTSendNodata(t *testing.T) { sender := NewMQTTSecretSender(MQTTSecretConfig{}, true) sender.mqttConfig = MQTTSecretConfig{} - continuePipeline, result := sender.MQTTSend(context) + continuePipeline, result := sender.MQTTSend(context, nil) require.False(t, continuePipeline) require.Error(t, result.(error)) } diff --git a/pkg/transforms/outputdata.go b/pkg/transforms/responsedata.go similarity index 54% rename from pkg/transforms/outputdata.go rename to pkg/transforms/responsedata.go index 86a78721b..5109863aa 100644 --- a/pkg/transforms/outputdata.go +++ b/pkg/transforms/responsedata.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 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,43 +17,42 @@ package transforms import ( + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" - - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" ) -// OutputData houses transform for outputting data to configured trigger response, i.e. message bus -type OutputData struct { +// ResponseData houses transform for outputting data to configured trigger response, i.e. message bus +type ResponseData struct { ResponseContentType string } -// NewOutputData creates, initializes and returns a new instance of OutputData -func NewOutputData() OutputData { - return OutputData{} +// NewResponseData creates, initializes and returns a new instance of ResponseData +func NewResponseData() ResponseData { + return ResponseData{} } -// SetOutputData sets the output data to that passed in from the previous function. -// It will return an error and stop the pipeline if the input data is not of type []byte, string or json.Mashaler -func (f OutputData) SetOutputData(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { +// SetResponseData sets the response data to that passed in from the previous function. +// It will return an error and stop the pipeline if the input data is not of type []byte, string or json.Marshaller +func (f ResponseData) SetResponseData(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("Setting output data") + ctx.LoggingClient().Debug("Setting response data") - if len(params) < 1 { + if data == nil { // We didn't receive a result return false, nil } - data, err := util.CoerceType(params[0]) + byteData, err := util.CoerceType(data) if err != nil { return false, err } if len(f.ResponseContentType) > 0 { - edgexcontext.ResponseContentType = f.ResponseContentType + ctx.SetResponseContentType(f.ResponseContentType) } // By setting this the data will be posted back to to configured trigger response, i.e. message bus - edgexcontext.OutputData = data + ctx.SetResponseData(byteData) - return true, params[0] + return true, data } diff --git a/pkg/transforms/outputdata_test.go b/pkg/transforms/responsedata_test.go similarity index 57% rename from pkg/transforms/outputdata_test.go rename to pkg/transforms/responsedata_test.go index 83397e80a..728a5c0fc 100644 --- a/pkg/transforms/outputdata_test.go +++ b/pkg/transforms/responsedata_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,34 +26,34 @@ import ( "github.com/stretchr/testify/require" ) -func TestSetOutputDataString(t *testing.T) { +func TestSetResponseDataString(t *testing.T) { expected := `0id1000` - target := NewOutputData() + target := NewResponseData() - continuePipeline, result := target.SetOutputData(context, expected) + continuePipeline, result := target.SetResponseData(context, expected) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, expected, actual) } -func TestSetOutputDataBytes(t *testing.T) { +func TestSetResponseDataBytes(t *testing.T) { var expected []byte expected = []byte(`0id1000`) - target := NewOutputData() + target := NewResponseData() - continuePipeline, result := target.SetOutputData(context, expected) + continuePipeline, result := target.SetResponseData(context, expected) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, string(expected), actual) } -func TestSetOutputDataEvent(t *testing.T) { - target := NewOutputData() +func TestSetResponseDataEvent(t *testing.T) { + target := NewResponseData() eventIn := dtos.Event{ DeviceName: deviceName1, @@ -61,38 +61,26 @@ func TestSetOutputDataEvent(t *testing.T) { expected, _ := json.Marshal(eventIn) - continuePipeline, result := target.SetOutputData(context, eventIn) + continuePipeline, result := target.SetResponseData(context, eventIn) assert.True(t, continuePipeline) assert.NotNil(t, result) - actual := string(context.OutputData) + actual := string(context.ResponseData()) assert.Equal(t, string(expected), actual) } -func TestSetOutputDataNoData(t *testing.T) { - target := NewOutputData() - continuePipeline, result := target.SetOutputData(context) +func TestSetResponseDataNoData(t *testing.T) { + target := NewResponseData() + continuePipeline, result := target.SetResponseData(context, nil) assert.Nil(t, result) assert.False(t, continuePipeline) } -func TestSetOutputDataMultipleParametersValid(t *testing.T) { - expected := `0id1000` - target := NewOutputData() - - continuePipeline, result := target.SetOutputData(context, expected, "", "", "") - assert.True(t, continuePipeline) - assert.NotNil(t, result) - - actual := string(context.OutputData) - assert.Equal(t, expected, actual) -} - -func TestSetOutputDataBadType(t *testing.T) { - target := NewOutputData() +func TestSetResponseDataBadType(t *testing.T) { + target := NewResponseData() // Channels are not marshalable to JSON and generate an error - continuePipeline, result := target.SetOutputData(context, make(chan int)) + continuePipeline, result := target.SetResponseData(context, make(chan int)) assert.False(t, continuePipeline) require.NotNil(t, result) assert.Contains(t, result.(error).Error(), "passed in data must be of type") diff --git a/pkg/transforms/tags.go b/pkg/transforms/tags.go index 943bfbac8..321914c61 100644 --- a/pkg/transforms/tags.go +++ b/pkg/transforms/tags.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,9 @@ import ( "errors" "fmt" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" ) // Tags contains the list of Tag key/values @@ -38,14 +38,14 @@ func NewTags(tags map[string]string) Tags { } // AddTags adds the pre-configured list of tags to the Event's tags collection. -func (t *Tags) AddTags(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) { - edgexcontext.LoggingClient.Debug("Adding tags to Event") +func (t *Tags) AddTags(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + ctx.LoggingClient().Debug("Adding tags to Event") - if len(params) < 1 { + if data == nil { return false, errors.New("no Event Received") } - event, ok := params[0].(dtos.Event) + event, ok := data.(dtos.Event) if !ok { return false, errors.New("type received is not an Event") } @@ -58,9 +58,9 @@ func (t *Tags) AddTags(edgexcontext *appcontext.Context, params ...interface{}) for tag, value := range t.tags { event.Tags[tag] = value } - edgexcontext.LoggingClient.Debug(fmt.Sprintf("Tags added to Event. Event tags=%v", event.Tags)) + ctx.LoggingClient().Debug(fmt.Sprintf("Tags added to Event. Event tags=%v", event.Tags)) } else { - edgexcontext.LoggingClient.Debug("No tags added to Event. Add tags list is empty.") + ctx.LoggingClient().Debug("No tags added to Event. Add tags list is empty.") } return true, event diff --git a/pkg/transforms/tags_test.go b/pkg/transforms/tags_test.go index c57244908..d055e6f37 100644 --- a/pkg/transforms/tags_test.go +++ b/pkg/transforms/tags_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 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,9 +19,6 @@ package transforms import ( "testing" - "github.com/edgexfoundry/app-functions-sdk-go/v2/appcontext" - - "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" "github.com/stretchr/testify/assert" @@ -50,10 +47,6 @@ var allTagsAdded = map[string]string{ } func TestTags_AddTags(t *testing.T) { - appContext := appcontext.Context{ - LoggingClient: logger.NewMockClient(), - } - tests := []struct { Name string FunctionInput interface{} @@ -77,9 +70,9 @@ func TestTags_AddTags(t *testing.T) { target := NewTags(testCase.TagsToAdd) if testCase.FunctionInput != nil { - continuePipeline, result = target.AddTags(&appContext, testCase.FunctionInput) + continuePipeline, result = target.AddTags(context, testCase.FunctionInput) } else { - continuePipeline, result = target.AddTags(&appContext) + continuePipeline, result = target.AddTags(context, nil) } if testCase.ErrorExpected { diff --git a/pkg/transforms/testmain_test.go b/pkg/transforms/testmain_test.go new file mode 100644 index 000000000..4eb1cbb78 --- /dev/null +++ b/pkg/transforms/testmain_test.go @@ -0,0 +1,60 @@ +// +// Copyright (c) 2021 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 transforms + +import ( + "os" + "testing" + + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/appfunction" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/bootstrap/container" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/common" + + bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v2/bootstrap/container" + "github.com/edgexfoundry/go-mod-bootstrap/v2/di" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/coredata" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/urlclient/local" +) + +var lc logger.LoggingClient +var dic *di.Container +var context *appfunction.Context + +func TestMain(m *testing.M) { + lc = logger.NewMockClient() + eventClient := coredata.NewEventClient(local.New("http://test" + clients.ApiEventRoute)) + + config := &common.ConfigurationStruct{} + + dic = di.NewContainer(di.ServiceConstructorMap{ + container.ConfigurationName: func(get di.Get) interface{} { + return config + }, + container.EventClientName: func(get di.Get) interface{} { + return eventClient + }, + bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} { + return lc + }, + }) + + context = appfunction.NewContext("123", dic, "") + + os.Exit(m.Run()) +} diff --git a/pkg/util/helpers.go b/pkg/util/helpers.go index 9e42a441c..688e0592e 100644 --- a/pkg/util/helpers.go +++ b/pkg/util/helpers.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ func DeleteEmptyAndTrim(s []string) []string { return r } -//CoerceType will accept a string, []byte, or json.Marshaler type and convert it to a []byte for use and consistency in the SDK +//CoerceType will accept a string, []byte, or json.Marshaller type and convert it to a []byte for use and consistency in the SDK func CoerceType(param interface{}) ([]byte, error) { var data []byte var err error diff --git a/pkg/util/helpers_test.go b/pkg/util/helpers_test.go index 7124282fd..476068626 100644 --- a/pkg/util/helpers_test.go +++ b/pkg/util/helpers_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -44,8 +44,8 @@ func TestSplitCommaEmpty(t *testing.T) { } func TestDeleteEmptyAndTrim(t *testing.T) { - strings := []string{" Hel lo", "test ", " "} - results := DeleteEmptyAndTrim(strings) + target := []string{" Hel lo", "test ", " "} + results := DeleteEmptyAndTrim(target) // Should have 4 elements (space counts as an element) assert.Equal(t, 2, len(results)) assert.Equal(t, "Hel lo", results[0]) From 81d29dc2373c1f1d909616355d508f0a0aa0ef72 Mon Sep 17 00:00:00 2001 From: lenny Date: Thu, 18 Mar 2021 11:01:07 -0700 Subject: [PATCH 3/3] refactor: Updates for PR review comments Signed-off-by: lenny --- app-service-template/functions/sample.go | 3 +-- internal/app/configurable.go | 12 ++++++------ internal/app/triggerfactory.go | 10 +++++----- internal/controller/rest/controller_test.go | 2 +- internal/runtime/runtime.go | 2 +- internal/runtime/storeforward.go | 2 +- internal/trigger/mqtt/mqtt.go | 10 +++++----- pkg/factory.go | 2 +- pkg/transforms/batch.go | 13 +++++-------- pkg/transforms/compression.go | 8 ++++---- pkg/transforms/http.go | 4 ++-- pkg/transforms/tags.go | 3 +-- 12 files changed, 33 insertions(+), 38 deletions(-) diff --git a/app-service-template/functions/sample.go b/app-service-template/functions/sample.go index 6e13fd82d..eefffb16b 100644 --- a/app-service-template/functions/sample.go +++ b/app-service-template/functions/sample.go @@ -18,7 +18,6 @@ package functions import ( "errors" - "fmt" "strings" "github.com/edgexfoundry/go-mod-core-contracts/v2/clients" @@ -129,7 +128,7 @@ func (s *Sample) OutputXML(ctx interfaces.AppFunctionContext, data interface{}) return false, errors.New("type received is not an string") } - lc.Debug(fmt.Sprintf("Outputting the following XML: %s", xml)) + lc.Debugf("Outputting the following XML: %s", xml) // This sends the XML as a response. i.e. publish for MessageBus/MQTT triggers as configured or // HTTP response to for the HTTP Trigger diff --git a/internal/app/configurable.go b/internal/app/configurable.go index 6b5701c56..1a796af6e 100644 --- a/internal/app/configurable.go +++ b/internal/app/configurable.go @@ -202,7 +202,7 @@ func (app *Configurable) PushToCore(parameters map[string]string) interfaces.App } readingName, ok := parameters[ReadingName] if !ok { - app.lc.Error("Could not find " + readingName) + app.lc.Error("Could not find " + ReadingName) return nil } deviceName = strings.TrimSpace(deviceName) @@ -399,7 +399,7 @@ func (app *Configurable) MQTTExport(parameters map[string]string) interfaces.App if ok { skipCertVerify, err = strconv.ParseBool(skipVerifyVal) if err != nil { - app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", skipVerifyVal, SkipVerify), "error", err) + app.lc.Errorf("Could not parse '%s' to a bool for '%s' parameter: %s", skipVerifyVal, SkipVerify, err.Error()) return nil } } @@ -420,7 +420,7 @@ func (app *Configurable) MQTTExport(parameters map[string]string) interfaces.App if ok { persistOnError, err = strconv.ParseBool(value) if err != nil { - app.lc.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err) + app.lc.Errorf("Could not parse '%s' to a bool for '%s' parameter: %s", value, PersistOnError, err.Error()) return nil } } @@ -547,16 +547,16 @@ func (app *Configurable) AddTags(parameters map[string]string) interfaces.AppFun for _, tag := range tagKeyValues { keyValue := util.DeleteEmptyAndTrim(strings.FieldsFunc(tag, util.SplitColon)) if len(keyValue) != 2 { - app.lc.Error(fmt.Sprintf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec)) + app.lc.Errorf("Bad Tags specification format. Expect comma separated list of 'key:value'. Got `%s`", tagsSpec) return nil } if len(keyValue[0]) == 0 { - app.lc.Error(fmt.Sprintf("Tag key missing. Got '%s'", tag)) + app.lc.Errorf("Tag key missing. Got '%s'", tag) return nil } if len(keyValue[1]) == 0 { - app.lc.Error(fmt.Sprintf("Tag value missing. Got '%s'", tag)) + app.lc.Errorf("Tag value missing. Got '%s'", tag) return nil } diff --git a/internal/app/triggerfactory.go b/internal/app/triggerfactory.go index 85c0f2c22..654e81278 100644 --- a/internal/app/triggerfactory.go +++ b/internal/app/triggerfactory.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2020 Technocrats +// Copyright (c) 2020 Technotects // Copyright (c) 2021 Intel Corporation // @@ -49,7 +49,7 @@ func (svc *Service) RegisterCustomTriggerFactory(name string, if nu == TriggerTypeMessageBus || nu == TriggerTypeHTTP || nu == TriggerTypeMQTT { - return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + return fmt.Errorf("cannot register custom trigger for builtin type (%s)", name) } if svc.customTriggerFactories == nil { @@ -71,7 +71,7 @@ func (svc *Service) RegisterCustomTriggerFactory(name string, func (svc *Service) defaultTriggerMessageProcessor(appContext interfaces.AppFunctionContext, envelope types.MessageEnvelope) error { context, ok := appContext.(*appfunction.Context) if !ok { - return fmt.Errorf("App Context must be an istance of internal appfunction.Context. Use NewAppContext to create instance.") + return errors.New("App Context must be an instance of internal appfunction.Context. Use NewAppContext to create instance.") } messageError := svc.runtime.ProcessMessage(context, envelope) @@ -109,11 +109,11 @@ func (svc *Service) setupTrigger(configuration *common.ConfigurationStruct, runt var err error t, err = factory(svc) if err != nil { - svc.LoggingClient().Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) + svc.LoggingClient().Errorf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error()) return nil } } else { - svc.LoggingClient().Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type)) + svc.LoggingClient().Errorf("Invalid Trigger type of '%s' specified", configuration.Trigger.Type) } } diff --git a/internal/controller/rest/controller_test.go b/internal/controller/rest/controller_test.go index 740341709..5627d5493 100644 --- a/internal/controller/rest/controller_test.go +++ b/internal/controller/rest/controller_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 202` Intel Corporation +// Copyright (c) 2021 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index b749a2935..8cbf4eceb 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -166,7 +166,7 @@ func (gr *GolangRuntime) ExecutePipeline( isRetry bool) *MessageError { var result interface{} - var continuePipeline = true + var continuePipeline bool for functionIndex, trxFunc := range transforms { if functionIndex < startPosition { diff --git a/internal/runtime/storeforward.go b/internal/runtime/storeforward.go index 39e54b2e8..7a735d95e 100644 --- a/internal/runtime/storeforward.go +++ b/internal/runtime/storeforward.go @@ -144,7 +144,7 @@ func (sf *storeForwardInfo) retryStoredData(serviceKey string) { return } - lc.Debug(fmt.Sprintf(" %d stored data items found for retrying", len(items))) + lc.Debugf(" %d stored data items found for retrying", len(items)) if len(items) > 0 { itemsToRemove, itemsToUpdate := sf.processRetryItems(items) diff --git a/internal/trigger/mqtt/mqtt.go b/internal/trigger/mqtt/mqtt.go index f89394729..3a1c700e5 100644 --- a/internal/trigger/mqtt/mqtt.go +++ b/internal/trigger/mqtt/mqtt.go @@ -109,7 +109,7 @@ func (trigger *Trigger) Initialize(_ *sync.WaitGroup, _ context.Context, backgro return nil, fmt.Errorf("unable to create secure MQTT Client: %s", err.Error()) } - lc.Info(fmt.Sprintf("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl)) + lc.Infof("Connecting to mqtt broker for MQTT trigger at: %s", brokerUrl) if token := mqttClient.Connect(); token.Wait() && token.Error() != nil { return nil, fmt.Errorf("could not connect to broker for MQTT trigger: %s", token.Error().Error()) @@ -137,8 +137,8 @@ func (trigger *Trigger) onConnectHandler(mqttClient pahoMqtt.Client) { for _, topic := range topics { if token := mqttClient.Subscribe(topic, qos, trigger.messageHandler); token.Wait() && token.Error() != nil { mqttClient.Disconnect(0) - lc.Error(fmt.Sprintf("could not subscribe to topic '%s' for MQTT trigger: %s", - topic, token.Error().Error())) + lc.Errorf("could not subscribe to topic '%s' for MQTT trigger: %s", + topic, token.Error().Error()) return } } @@ -182,10 +182,10 @@ func (trigger *Trigger) messageHandler(client pahoMqtt.Client, message pahoMqtt. if len(appContext.ResponseData()) > 0 && len(topic) > 0 { if token := client.Publish(topic, brokerConfig.QoS, brokerConfig.Retain, appContext.ResponseData); token.Wait() && token.Error() != nil { - lc.Error("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) + lc.Errorf("could not publish to topic '%s' for MQTT trigger: %s", topic, token.Error().Error()) } else { lc.Trace("Sent MQTT Trigger response message", clients.CorrelationHeader, correlationID) - lc.Debug(fmt.Sprintf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(appContext.ResponseData()))) + lc.Debugf("Sent MQTT Trigger response message on topic '%s' with %d bytes", topic, len(appContext.ResponseData())) } } } diff --git a/pkg/factory.go b/pkg/factory.go index 1fc7030be..0befbaf4a 100644 --- a/pkg/factory.go +++ b/pkg/factory.go @@ -35,7 +35,7 @@ func NewAppService(serviceKey string) (interfaces.ApplicationService, bool) { return NewAppServiceWithTargetType(serviceKey, nil) } -// NewAppService creates and returns a new ApplicationService with the specified TargetType +// NewAppServiceWithTargetType creates and returns a new ApplicationService with the specified TargetType func NewAppServiceWithTargetType(serviceKey string, targetType interface{}) (interfaces.ApplicationService, bool) { service := app.NewService(serviceKey, targetType, interfaces.ProfileSuffixPlaceholder) if err := service.Initialize(); err != nil { diff --git a/pkg/transforms/batch.go b/pkg/transforms/batch.go index 0434ff80a..e2dd4cf19 100644 --- a/pkg/transforms/batch.go +++ b/pkg/transforms/batch.go @@ -142,14 +142,11 @@ func (batch *BatchConfig) Batch(ctx interfaces.AppFunctionContext, data interfac if batch.batchMode != BatchByCountOnly { if !batch.timerActive.Value() { batch.timerActive.Set(true) - for { - select { - case <-batch.done: - ctx.LoggingClient().Debug("Batch count has been reached") - case <-time.After(batch.parsedDuration): - ctx.LoggingClient().Debug("Timer has elapsed") - } - break + select { + case <-batch.done: + ctx.LoggingClient().Debug("Batch count has been reached") + case <-time.After(batch.parsedDuration): + ctx.LoggingClient().Debug("Timer has elapsed") } batch.timerActive.Set(false) } else { diff --git a/pkg/transforms/compression.go b/pkg/transforms/compression.go index 8099c096f..58c19a885 100644 --- a/pkg/transforms/compression.go +++ b/pkg/transforms/compression.go @@ -63,12 +63,12 @@ func (compression *Compression) CompressWithGZIP(ctx interfaces.AppFunctionConte _, err = compression.gzipWriter.Write(rawData) if err != nil { - return false, fmt.Errorf("unable to write GZIP data") + return false, fmt.Errorf("unable to write GZIP data: %s", err.Error()) } err = compression.gzipWriter.Close() if err != nil { - return false, fmt.Errorf("unable to close GZIP data") + return false, fmt.Errorf("unable to close GZIP data: %s", err.Error()) } // Set response "content-type" header to "text/plain" @@ -100,12 +100,12 @@ func (compression *Compression) CompressWithZLIB(ctx interfaces.AppFunctionConte _, err = compression.zlibWriter.Write(byteData) if err != nil { - return false, fmt.Errorf("unable to write ZLIB data") + return false, fmt.Errorf("unable to write ZLIB data: %s", err.Error()) } err = compression.zlibWriter.Close() if err != nil { - return false, fmt.Errorf("unable to close ZLIB data") + return false, fmt.Errorf("unable to close ZLIB data: %s", err.Error()) } // Set response "content-type" header to "text/plain" diff --git a/pkg/transforms/http.go b/pkg/transforms/http.go index 5d90f872b..f6fb6e0cf 100644 --- a/pkg/transforms/http.go +++ b/pkg/transforms/http.go @@ -125,8 +125,8 @@ func (sender HTTPSender) httpSend(ctx interfaces.AppFunctionContext, data interf return false, err } defer func() { _ = response.Body.Close() }() - ctx.LoggingClient().Debug(fmt.Sprintf("Response: %s", response.Status)) - ctx.LoggingClient().Debug(fmt.Sprintf("Sent data: %s", string(exportData))) + ctx.LoggingClient().Debugf("Response: %s", response.Status) + ctx.LoggingClient().Debugf("Sent data: %s", string(exportData)) bodyBytes, errReadingBody := ioutil.ReadAll(response.Body) if errReadingBody != nil { sender.setRetryData(ctx, exportData) diff --git a/pkg/transforms/tags.go b/pkg/transforms/tags.go index 321914c61..d61666186 100644 --- a/pkg/transforms/tags.go +++ b/pkg/transforms/tags.go @@ -18,7 +18,6 @@ package transforms import ( "errors" - "fmt" "github.com/edgexfoundry/go-mod-core-contracts/v2/v2/dtos" @@ -58,7 +57,7 @@ func (t *Tags) AddTags(ctx interfaces.AppFunctionContext, data interface{}) (boo for tag, value := range t.tags { event.Tags[tag] = value } - ctx.LoggingClient().Debug(fmt.Sprintf("Tags added to Event. Event tags=%v", event.Tags)) + ctx.LoggingClient().Debugf("Tags added to Event. Event tags=%v", event.Tags) } else { ctx.LoggingClient().Debug("No tags added to Event. Add tags list is empty.") }