Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add custom structured configuration capability #753

Merged
merged 5 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app-service-template/Attribution.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,7 @@ golang.org/x/sys (Unspecified) https://github.com/golang/sys
https://github.com/golang/sys/blob/master/LICENSE

stretchr/objx (MIT) https://github.com/stretchr/objx
https://github.com/stretchr/objx/blob/master/LICENSE
https://github.com/stretchr/objx/blob/master/LICENSE

github.com/gorilla/websocket (BSD-2) https://github.com/gorilla/websocket
https://github.com/gorilla/websocket/blob/master/LICENSE
80 changes: 80 additions & 0 deletions app-service-template/config/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 config

// This file contains example of custom configuration that can be loaded from the service's configuration.toml
// and/or the Configuration Provider, aka Consul (if enabled).
// For more details see https://docs.edgexfoundry.org/2.0/microservices/application/GeneralAppServiceConfig/#custom-configuration
// TODO: Update this configuration as needed for you service's needs and remove this comment
// or remove this file if not using custom configuration.

import (
"errors"
"reflect"
)

// TODO: Define your structured custom configuration types. Must be wrapped with an outer struct with
// single element that matches the top level custom configuration element in your configuration.toml file,
// 'AppCustom' in this example. Replace this example with your configuration structure or
// remove this file if not using structured custom configuration.
type ServiceConfig struct {
AppCustom AppCustomConfig
}

// AppCustomConfig is example of service's custom structured configuration that is specified in the service's
// configuration.toml file and Configuration Provider (aka Consul), if enabled.
type AppCustomConfig struct {
ResourceNames string
SomeValue int
SomeService HostInfo
}

// HostInfo is example struct for defining connection information for external service
type HostInfo struct {
Host string
Port int
Protocol string
}

// TODO: Update using your Custom configuration type.
// UpdateFromRaw updates the service's full configuration from raw data received from
// the Service Provider.
func (c *ServiceConfig) UpdateFromRaw(rawConfig interface{}) bool {
configuration, ok := rawConfig.(*ServiceConfig)
if !ok {
return false //errors.New("unable to cast raw config to type 'ServiceConfig'")
}

*c = *configuration

return true
}

// Validate ensures your custom configuration has proper values.
// TODO: Update to properly validate your custom configuration
func (ac *AppCustomConfig) Validate() error {
if ac.SomeValue <= 0 {
return errors.New("SomeValue must be greater than zero")
}

if reflect.DeepEqual(ac.SomeService, HostInfo{}) {
return errors.New("SomeService is not set")
}

return nil
}
102 changes: 87 additions & 15 deletions app-service-template/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ package main

import (
"os"
"reflect"

"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger"

"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/config"

"new-app-service/functions"
)
Expand All @@ -31,49 +35,117 @@ const (
serviceKey = "new-app-service"
)

// TODO: Define your app's struct
type myApp struct {
service interfaces.ApplicationService
lc logger.LoggingClient
serviceConfig *config.ServiceConfig
configChanged chan bool
}

func main() {
// TODO: See https://docs.edgexfoundry.org/1.3/microservices/application/ApplicationServices/
// TODO: See https://docs.edgexfoundry.org/2.0/microservices/application/ApplicationServices/
// for documentation on application services.

code := CreateAndRunService(serviceKey, pkg.NewAppService)
app := myApp{}
code := app.CreateAndRunAppService(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)
// CreateAndRunAppService wraps what would normally be in main() so that it can be unit tested
// TODO: Remove and just use regular main() if unit tests of main logic not needed.
func (app *myApp) CreateAndRunAppService(serviceKey string, newServiceFactory func(string) (interfaces.ApplicationService, bool)) int {
var ok bool
app.service, ok = newServiceFactory(serviceKey)
if !ok {
return -1
}

lc := service.LoggingClient()
app.lc = app.service.LoggingClient()

// TODO: Replace with retrieving your custom ApplicationSettings from configuration
deviceNames, err := service.GetAppSettingStrings("DeviceNames")
// TODO: Replace with retrieving your custom ApplicationSettings from configuration or
// remove if not using AppSetting configuration section.
// For more details see https://docs.edgexfoundry.org/2.0/microservices/application/GeneralAppServiceConfig/#application-settings
deviceNames, err := app.service.GetAppSettingStrings("DeviceNames")
if err != nil {
lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error())
app.lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error())
return -1
}

// More advance custom structured configuration can be defined and loaded as in this example.
// For more details see https://docs.edgexfoundry.org/2.0/microservices/application/GeneralAppServiceConfig/#custom-configuration
// TODO: Change to use your service's custom configuration struct
// or remove if not using custom configuration capability
app.serviceConfig = &config.ServiceConfig{}
if err := app.service.LoadCustomConfig(app.serviceConfig, "AppCustom"); err != nil {
app.lc.Errorf("failed load custom configuration: %s", err.Error())
return -1
}

// Optionally validate the custom configuration after it is loaded.
// TODO: remove if you don't have custom configuration or don't need to validate it
if err := app.serviceConfig.AppCustom.Validate(); err != nil {
app.lc.Errorf("custom configuration failed validation: %s", err.Error())
return -1
}

// Custom configuration can be 'writable' or a section of the configuration can be 'writable' when using
// the Configuration Provider, aka Consul.
// For more details see https://docs.edgexfoundry.org/2.0/microservices/application/GeneralAppServiceConfig/#writable-custom-configuration
// TODO: Remove if not using writable custom configuration
if err := app.service.ListenForCustomConfigChanges(&app.serviceConfig.AppCustom, "AppCustom", app.ProcessConfigUpdates); err != nil {
app.lc.Errorf("unable to watch custom writable 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
// See https://docs.edgexfoundry.org/2.0/microservices/application/BuiltIn/ for list of built-in functions
sample := functions.NewSample()
err = service.SetFunctionsPipeline(
err = app.service.SetFunctionsPipeline(
transforms.NewFilterFor(deviceNames).FilterByDeviceName,
sample.LogEventDetails,
sample.ConvertEventToXML,
sample.OutputXML)
if err != nil {
lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error())
app.lc.Errorf("SetFunctionsPipeline returned error: %s", err.Error())
return -1
}

if err := service.MakeItRun(); err != nil {
lc.Errorf("MakeItRun returned error: %s", err.Error())
if err := app.service.MakeItRun(); err != nil {
app.lc.Errorf("MakeItRun returned error: %s", err.Error())
return -1
}

// TODO: Do any required cleanup here, if needed

return 0
}

// TODO: Update using your Custom configuration 'writeable' type or remove if not using ListenForCustomConfigChanges
// ProcessConfigUpdates processes the updated configuration for the service's writable configuration.
// At a minimum it must copy the updated configuration into the service's current configuration. Then it can
// do any special processing for changes that require more.
func (app *myApp) ProcessConfigUpdates(rawWritableConfig interface{}) {
updated, ok := rawWritableConfig.(*config.AppCustomConfig)
if !ok {
app.lc.Error("unable to process config updates: Can not cast raw config to type 'AppCustomConfig'")
return
}

previous := app.serviceConfig.AppCustom
app.serviceConfig.AppCustom = *updated

if reflect.DeepEqual(previous, updated) {
app.lc.Info("No changes detected")
return
}

if previous.SomeValue != updated.SomeValue {
app.lc.Infof("AppCustom.SomeValue changed to: %d", updated.SomeValue)
}
if previous.ResourceNames != updated.ResourceNames {
app.lc.Infof("AppCustom.ResourceNames changed to: %s", updated.ResourceNames)
}
if !reflect.DeepEqual(previous.SomeService, updated.SomeService) {
app.lc.Infof("AppCustom.SomeService changed to: %v", updated.SomeService)
}
}
69 changes: 60 additions & 9 deletions app-service-template/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,88 +24,139 @@ import (
"github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"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()
// TODO: add/update tests for your customized CreateAndRunAppService or remove if your main code doesn't require unit testing.

func TestCreateAndRunService_Success(t *testing.T) {
app := myApp{}

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("LoadCustomConfig", mock.Anything, mock.Anything, mock.Anything).
Return(nil).Run(func(args mock.Arguments) {
// set the required configuration so validation passes
app.serviceConfig.AppCustom.SomeValue = 987
app.serviceConfig.AppCustom.SomeService.Host = "SomeHost"
})
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("MakeItRun").Return(nil)

return mockAppService, true
}

expected := 0
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
assert.Equal(t, expected, actual)
}

func TestCreateAndRunService_NewService_Failed(t *testing.T) {
app := myApp{}

mockFactory := func(_ string) (interfaces.ApplicationService, bool) {
return nil, false
}
expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
assert.Equal(t, expected, actual)
}

func TestCreateAndRunService_GetAppSettingStrings_Failed(t *testing.T) {
app := myApp{}

getAppSettingStringsCalled := false
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(nil, fmt.Errorf("Failed")).Run(func(args mock.Arguments) {
getAppSettingStringsCalled = true
})

return mockAppService, true
}

expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
require.True(t, getAppSettingStringsCalled, "GetAppSettingStrings never called")
assert.Equal(t, expected, actual)
}

func TestCreateAndRunService_SetFunctionsPipeline_Failed(t *testing.T) {
app := myApp{}

// ensure failure is from SetFunctionsPipeline
setFunctionsPipelineCalled := false

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("LoadCustomConfig", mock.Anything, mock.Anything, mock.Anything).
Return(nil).Run(func(args mock.Arguments) {
// set the required configuration so validation passes
app.serviceConfig.AppCustom.SomeValue = 987
app.serviceConfig.AppCustom.SomeService.Host = "SomeHost"
})
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(fmt.Errorf("Failed"))
Return(fmt.Errorf("Failed")).Run(func(args mock.Arguments) {
setFunctionsPipelineCalled = true
})

return mockAppService, true
}

expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
require.True(t, setFunctionsPipelineCalled, "SetFunctionsPipeline never called")
assert.Equal(t, expected, actual)
}

func TestCreateAndRunService_MakeItRun_Failed(t *testing.T) {
app := myApp{}

// ensure failure is from MakeItRun
makeItRunCalled := false

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("LoadCustomConfig", mock.Anything, mock.Anything, mock.Anything).
Return(nil).Run(func(args mock.Arguments) {
// set the required configuration so validation passes
app.serviceConfig.AppCustom.SomeValue = 987
app.serviceConfig.AppCustom.SomeService.Host = "SomeHost"
})
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("MakeItRun").Return(fmt.Errorf("Failed"))
mockAppService.On("MakeItRun").Return(fmt.Errorf("Failed")).Run(func(args mock.Arguments) {
makeItRunCalled = true
})

return mockAppService, true
}

expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
require.True(t, makeItRunCalled, "MakeItRun never called")
assert.Equal(t, expected, actual)
}
Loading