From c18f6a6fadc43382d207a43ccfac79720fe4f269 Mon Sep 17 00:00:00 2001 From: Alex Ullrich Date: Sun, 6 Dec 2020 15:20:49 -0500 Subject: [PATCH] feat(sdk): Enable Custom Trigger Registration accept registration of factory functions for making custom trigger types available to the SDK Signed-off-by: Alex Ullrich --- appsdk/sdk.go | 31 +--- {internal/trigger => appsdk}/trigger.go | 19 ++- appsdk/triggerfactory.go | 117 +++++++++++++++ appsdk/triggerfactory_test.go | 192 ++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 31 deletions(-) rename {internal/trigger => appsdk}/trigger.go (54%) create mode 100644 appsdk/triggerfactory.go create mode 100644 appsdk/triggerfactory_test.go diff --git a/appsdk/sdk.go b/appsdk/sdk.go index 17fdfafa6..14de126f5 100644 --- a/appsdk/sdk.go +++ b/appsdk/sdk.go @@ -37,10 +37,6 @@ import ( "github.com/edgexfoundry/app-functions-sdk-go/internal/common" "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" "github.com/edgexfoundry/app-functions-sdk-go/internal/store/db/interfaces" - "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger" - "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http" - "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus" - "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt" "github.com/edgexfoundry/app-functions-sdk-go/internal/webserver" "github.com/edgexfoundry/app-functions-sdk-go/pkg/util" @@ -129,6 +125,7 @@ type AppFunctionsSDK struct { deferredFunctions []bootstrap.Deferred serviceKeyOverride string backgroundChannel <-chan types.MessageEnvelope + customTriggerBuilders map[string]func(sdk *AppFunctionsSDK) (Trigger, error) } // AddRoute allows you to leverage the existing webserver to add routes. @@ -458,32 +455,6 @@ func (sdk *AppFunctionsSDK) StoreSecrets(path string, secrets map[string]string) return sdk.secretProvider.StoreSecrets(path, secrets) } -// setupTrigger configures the appropriate trigger as specified by configuration. -func (sdk *AppFunctionsSDK) setupTrigger(configuration *common.ConfigurationStruct, runtime *runtime.GolangRuntime) trigger.Trigger { - var t trigger.Trigger - // Need to make dynamic, search for the binding that is input - - switch strings.ToUpper(configuration.Binding.Type) { - case bindingTypeHTTP: - sdk.LoggingClient.Info("HTTP trigger selected") - t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients} - - case bindingTypeMessageBus, - bindingTypeEdgeXMessageBus: // Allows for more explicit name now that we have plain MQTT option also - sdk.LoggingClient.Info("EdgeX MessageBus trigger selected") - t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients} - - case bindingTypeMQTT: - sdk.LoggingClient.Info("External MQTT trigger selected") - t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider) - - default: - sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Binding.Type)) - } - - return t -} - 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) diff --git a/internal/trigger/trigger.go b/appsdk/trigger.go similarity index 54% rename from internal/trigger/trigger.go rename to appsdk/trigger.go index 2609741e3..938924b0d 100644 --- a/internal/trigger/trigger.go +++ b/appsdk/trigger.go @@ -14,10 +14,13 @@ // limitations under the License. // -package trigger +package appsdk import ( "context" + "github.com/edgexfoundry/app-functions-sdk-go/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" "github.com/edgexfoundry/go-mod-messaging/pkg/types" "sync" @@ -29,3 +32,17 @@ 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 + +// 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 fo +type TriggerConfig struct { + Config *common.ConfigurationStruct + Logger logger.LoggingClient + ContextBuilder TriggerContextBuilder + MessageProcessor TriggerMessageProcessor +} diff --git a/appsdk/triggerfactory.go b/appsdk/triggerfactory.go new file mode 100644 index 000000000..bdd3aa650 --- /dev/null +++ b/appsdk/triggerfactory.go @@ -0,0 +1,117 @@ +// +// Copyright (c) 2020 Technotects +// +// 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" + "github.com/edgexfoundry/app-functions-sdk-go/appcontext" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/runtime" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt" + "github.com/edgexfoundry/go-mod-messaging/pkg/types" + "strings" +) + +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, + } +} + +// RegisterCustomTrigger allows users to register builders for custom trigger types +func (s *AppFunctionsSDK) RegisterCustomTrigger(name string, + builder func(TriggerConfig) (Trigger, error)) error { + nu := strings.ToUpper(name) + + if nu == bindingTypeEdgeXMessageBus || + nu == bindingTypeMessageBus || + nu == bindingTypeHTTP || + nu == bindingTypeMQTT { + return errors.New(fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + } + + if s.customTriggerBuilders == nil { + s.customTriggerBuilders = make(map[string]func(sdk *AppFunctionsSDK) (Trigger, error), 1) + } + + s.customTriggerBuilders[nu] = func(sdk *AppFunctionsSDK) (Trigger, error) { + return builder(TriggerConfig{ + Config: s.config, + Logger: s.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 binding that is input + + switch triggerType := strings.ToUpper(configuration.Binding.Type); triggerType { + case bindingTypeHTTP: + sdk.LoggingClient.Info("HTTP trigger selected") + t = &http.Trigger{Configuration: configuration, Runtime: runtime, Webserver: sdk.webserver, EdgeXClients: sdk.EdgexClients} + + case bindingTypeMessageBus, + bindingTypeEdgeXMessageBus: // Allows for more explicit name now that we have plain MQTT option also + sdk.LoggingClient.Info("EdgeX MessageBus trigger selected") + t = &messagebus.Trigger{Configuration: configuration, Runtime: runtime, EdgeXClients: sdk.EdgexClients} + + case bindingTypeMQTT: + sdk.LoggingClient.Info("External MQTT trigger selected") + t = mqtt.NewTrigger(configuration, runtime, sdk.EdgexClients, sdk.secretProvider) + + default: + if builder, found := sdk.customTriggerBuilders[triggerType]; found { + var err error + t, err = builder(sdk) + if err != nil { + sdk.LoggingClient.Error(fmt.Sprintf("failed to initialize custom trigger [%s]: %s", triggerType, err.Error())) + panic(err) + } + } else { + sdk.LoggingClient.Error(fmt.Sprintf("Invalid Trigger type of '%s' specified", configuration.Binding.Type)) + } + } + + return t +} diff --git a/appsdk/triggerfactory_test.go b/appsdk/triggerfactory_test.go new file mode 100644 index 000000000..1a5736c1c --- /dev/null +++ b/appsdk/triggerfactory_test.go @@ -0,0 +1,192 @@ +// +// Copyright (c) 2020 Technotects +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package appsdk + +import ( + "context" + "fmt" + "github.com/edgexfoundry/app-functions-sdk-go/internal/common" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/http" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/messagebus" + "github.com/edgexfoundry/app-functions-sdk-go/internal/trigger/mqtt" + "github.com/edgexfoundry/go-mod-bootstrap/bootstrap" + "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" + "github.com/edgexfoundry/go-mod-messaging/pkg/types" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "strings" + "sync" + "testing" +) + +func TestRegisterCustomTrigger_HTTP(t *testing.T) { + name := strings.ToTitle(bindingTypeHTTP) + + sdk := AppFunctionsSDK{} + err := sdk.RegisterCustomTrigger(name, nil) + + require.NotNil(t, err, "should throw error") + require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered") +} + +func TestRegisterCustomTrigger_MessageBus(t *testing.T) { + name := strings.ToTitle(bindingTypeMessageBus) + + sdk := AppFunctionsSDK{} + err := sdk.RegisterCustomTrigger(name, nil) + + require.NotNil(t, err, "should throw error") + require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered") +} + +func TestRegisterCustomTrigger_EdgeXMessageBus(t *testing.T) { + name := strings.ToTitle(bindingTypeEdgeXMessageBus) + + sdk := AppFunctionsSDK{} + err := sdk.RegisterCustomTrigger(name, nil) + + require.NotNil(t, err, "should throw error") + require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered") +} + +func TestRegisterCustomTrigger_MQTT(t *testing.T) { + name := strings.ToTitle(bindingTypeMQTT) + + sdk := AppFunctionsSDK{} + err := sdk.RegisterCustomTrigger(name, nil) + + require.NotNil(t, err, "should throw error") + require.NotNil(t, err.Error(), fmt.Sprintf("cannot register custom trigger for builtin type (%s)", name)) + require.Zero(t, len(sdk.customTriggerBuilders), "nothing should be registered") +} + +func TestRegisterCustomTrigger(t *testing.T) { + name := "cUsToM tRiGgEr" + trig := mockCustomTrigger{} + + builder := func(c TriggerConfig) (Trigger, error) { + return &trig, nil + } + sdk := AppFunctionsSDK{} + err := sdk.RegisterCustomTrigger(name, builder) + + require.Nil(t, err, "should not throw error") + require.Equal(t, len(sdk.customTriggerBuilders), 1, "provided function should be registered") + + registeredBuilder := sdk.customTriggerBuilders[strings.ToUpper(name)] + require.NotNil(t, registeredBuilder, "provided function should be registered with uppercase name") + + res, err := registeredBuilder(&sdk) + require.NoError(t, err) + require.Equal(t, res, &trig, "will be wrapped but should ultimately return result of builder") +} + +func TestSetupTrigger_HTTP(t *testing.T) { + sdk := AppFunctionsSDK{ + config: &common.ConfigurationStruct{ + Binding: common.BindingInfo{ + Type: "http", + }, + }, + LoggingClient: logger.MockLogger{}, + } + + trigger := sdk.setupTrigger(sdk.config, sdk.runtime) + + require.NotNil(t, trigger, "should be defined") + require.IsType(t, &http.Trigger{}, trigger, "should be an http trigger") +} + +func TestSetupTrigger_MessageBus(t *testing.T) { + sdk := AppFunctionsSDK{ + config: &common.ConfigurationStruct{ + Binding: common.BindingInfo{ + Type: "messagebus", + }, + }, + LoggingClient: logger.MockLogger{}, + } + + trigger := sdk.setupTrigger(sdk.config, sdk.runtime) + + require.NotNil(t, trigger, "should be defined") + require.IsType(t, &messagebus.Trigger{}, trigger, "should be a messagebus trigger") +} + +func TestSetupTrigger_EdgeXMessageBus(t *testing.T) { + sdk := AppFunctionsSDK{ + config: &common.ConfigurationStruct{ + Binding: common.BindingInfo{ + Type: "edgex-messagebus", + }, + }, + LoggingClient: logger.MockLogger{}, + } + + trigger := sdk.setupTrigger(sdk.config, sdk.runtime) + + require.NotNil(t, trigger, "should be defined") + require.IsType(t, &messagebus.Trigger{}, trigger, "should be a messagebus trigger") +} + +func TestSetupTrigger_MQTT(t *testing.T) { + sdk := AppFunctionsSDK{ + config: &common.ConfigurationStruct{ + Binding: common.BindingInfo{ + Type: "external-mqtt", + }, + }, + LoggingClient: logger.MockLogger{}, + } + + trigger := sdk.setupTrigger(sdk.config, sdk.runtime) + + require.NotNil(t, trigger, "should be defined") + require.IsType(t, &mqtt.Trigger{}, trigger, "should be an MQTT trigger") +} + +type mockCustomTrigger struct { +} + +func (*mockCustomTrigger) Initialize(wg *sync.WaitGroup, ctx context.Context, background <-chan types.MessageEnvelope) (bootstrap.Deferred, error) { + return nil, nil +} + +func TestSetupTrigger_CustomType(t *testing.T) { + triggerName := uuid.New().String() + + sdk := AppFunctionsSDK{ + config: &common.ConfigurationStruct{ + Binding: common.BindingInfo{ + Type: triggerName, + }, + }, + LoggingClient: logger.MockLogger{}, + } + + sdk.RegisterCustomTrigger(triggerName, func(c TriggerConfig) (Trigger, error) { + return &mockCustomTrigger{}, nil + }) + + trigger := sdk.setupTrigger(sdk.config, sdk.runtime) + + require.NotNil(t, trigger, "should be defined") + require.IsType(t, &mockCustomTrigger{}, trigger, "should be a custom trigger") +}