Skip to content

Commit

Permalink
feat: Add custom structured configuration capability
Browse files Browse the repository at this point in the history
Includes capability to watch a section of the custom configuration for changes when using the Configuration Provider

closes #557 & #578

Signed-off-by: lenny <[email protected]>
  • Loading branch information
lenny committed Mar 22, 2021
1 parent c6680ff commit 772a84f
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 43 deletions.
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
127 changes: 127 additions & 0 deletions app-service-template/config/configuration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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"

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

// 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
}

// TODO: Update using your Custom configuration 'writeable' type or remove if not using ListenForCustomConfigChanges
// UpdateWritableFromRaw updates the service's writable configuration from raw data received from
// the Service Provider. Must implement if using ListenForCustomConfigChanges, otherwise this can be removed.
func (ac *AppCustomConfig) UpdateWritableFromRaw(rawWritableConfig interface{}) bool {
appCustom, ok := rawWritableConfig.(*AppCustomConfig)
if !ok {
return false //errors.New("unable to cast raw writeable config to type 'AppCustomConfig'")
}

*ac = *appCustom

return true
}

// WaitForCustomConfigChanges waits for indication that the custom configuration section has been updated and then process
// the changes as needed
// TODO: Update to use your custom configuration section that you want to be writable (i.e. runtime changes from Consul)
// or remove if not using custom configuration section or writable custom configuration.
func (ac *AppCustomConfig) WaitForCustomConfigChanges(configChanged chan bool, lc logger.LoggingClient) {
previous := *ac // Copy for change detection

go func() {
for {
select {
case <-configChanged:
// TODO: Process the changed configuration.
// Must keep a previous copy of the configuration to determine what has changed.
// Replace the examples below with your appropriate processing logic.
switch {
case previous.SomeValue != ac.SomeValue:
lc.Infof("AppCustom.SomeValue changed to: %d", ac.SomeValue)
case previous.ResourceNames != ac.ResourceNames:
lc.Infof("AppCustom.ResourceNames changed to: %s", ac.ResourceNames)
case !reflect.DeepEqual(previous.SomeService, ac.SomeService):
lc.Infof("AppCustom.SomeService changed to: %v", ac.SomeService)
default:
lc.Info("No changes detected")
}

previous = *ac
}
}
}()
}

// 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
}
74 changes: 59 additions & 15 deletions app-service-template/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ package main
import (
"os"

"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,45 +34,86 @@ 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 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 {
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
}

// TODO: Replace with retrieving your custom ApplicationSettings from configuration
deviceNames, err := service.GetAppSettingStrings("DeviceNames")
// 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
configChanged := make(chan bool)
err = app.service.ListenForCustomConfigChanges(&app.serviceConfig.AppCustom, "AppCustom", configChanged)
if err != nil {
lc.Errorf("failed to retrieve DeviceNames from configuration: %s", err.Error())
app.lc.Errorf("unable to watch custom writable configuration: %s", err.Error())
return -1
}
app.serviceConfig.AppCustom.WaitForCustomConfigChanges(configChanged, app.lc)

// 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
}

Expand Down
29 changes: 23 additions & 6 deletions app-service-template/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (

// 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) {
mockFactory := func(_ string) (interfaces.ApplicationService, bool) {
Expand All @@ -41,22 +41,28 @@ func TestCreateAndRunService_Success(t *testing.T) {
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)
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("MakeItRun").Return(nil)

return mockAppService, true
}

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

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

Expand All @@ -70,8 +76,9 @@ func TestCreateAndRunService_GetAppSettingStrings_Failed(t *testing.T) {
return mockAppService, true
}

app := myApp{}
expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
assert.Equal(t, expected, actual)
}

Expand All @@ -81,14 +88,19 @@ func TestCreateAndRunService_SetFunctionsPipeline_Failed(t *testing.T) {
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)
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(nil)
mockAppService.On("SetFunctionsPipeline", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(fmt.Errorf("Failed"))

return mockAppService, true
}

app := myApp{}
expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
assert.Equal(t, expected, actual)
}

Expand All @@ -98,14 +110,19 @@ func TestCreateAndRunService_MakeItRun_Failed(t *testing.T) {
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)
mockAppService.On("ListenForCustomConfigChanges", mock.Anything, 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"))

return mockAppService, true
}

app := myApp{}
expected := -1
actual := CreateAndRunService("TestKey", mockFactory)
actual := app.CreateAndRunAppService("TestKey", mockFactory)
assert.Equal(t, expected, actual)
}
19 changes: 15 additions & 4 deletions app-service-template/res/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ RetryWaitPeriod = "1s"
AuthType = 'X-Vault-Token'

[Clients]
[Clients.CoreData]
[Clients.edgex-core-data]
Protocol = 'http'
Host = 'localhost'
Port = 48080
Expand Down Expand Up @@ -102,10 +102,21 @@ PublishTopic="event-xml" #TODO: remove if service is NOT publishing back to the
# ConnectTimeout = "30" # Seconds
# SkipCertVerify = "false"


[ApplicationSettings]
# TODO: Add custom settings needed by your app service
# TODO: Add custom settings needed by your app service or remove if you don't have any settings.
# This can be any Key/Value pair you need.
# For more details see: https://docs.edgexfoundry.org/1.3/microservices/application/GeneralAppServiceConfig/#application-settings
# Example that works with devices from the Virtual Device service:
[ApplicationSettings]
DeviceNames = "Random-Boolean-Device, Random-Integer-Device, Random-UnsignedInteger-Device, Random-Float-Device, Random-Binary-Device"

# TODO: Replace this section with your actual structured custom configuration section
# or remove if you don't have a need for structured custom configuration
# This can be any structure you need, but it can not contain slices. Use a maps instead of slices.
# For more details see: https://docs.edgexfoundry.org/2.0/microservices/application/GeneralAppServiceConfig/#custom-configuration
[AppCustom]
ResourceNames = "Boolean, Int32, Uint32, Float32, Binary"
SomeValue = 123
[AppCustom.SomeService]
Host = "localhost"
Port = 9080
Protocol = "http"
Loading

0 comments on commit 772a84f

Please sign in to comment.