Skip to content

Commit

Permalink
feat: Add command line/environment flag for commonConfig (#487)
Browse files Browse the repository at this point in the history
* feat: Add command line/environment flag for commonConfig

Signed-off-by: Elizabeth J Lee <[email protected]>
---------

Signed-off-by: Elizabeth J Lee <[email protected]>
  • Loading branch information
ejlee3 authored Mar 22, 2023
1 parent 9d98d1e commit fed18d9
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 22 deletions.
96 changes: 88 additions & 8 deletions bootstrap/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ import (

const (
writableKey = "/Writable"
allServicesKey = "/all-services"
appServicesKey = "/app-services"
deviceServicesKey = "/device-services"
allServicesKey = "all-services"
appServicesKey = "app-services"
deviceServicesKey = "device-services"
sep = "/"
)

// UpdatedStream defines the stream type that is notified by ListenForChanges when a configuration update is received.
Expand Down Expand Up @@ -176,9 +177,24 @@ func (cp *Processor) Process(

cp.lc.Info("Private configuration loaded from the Configuration Provider. No overrides applied")
}
} else {
// Now load common configuration from local file if not using config provider
commonConfigLocation := environment.GetCommonConfigFileName(cp.lc, cp.flags.CommonConfig())
if commonConfigLocation != "" {
err := cp.loadCommonConfigFromFile(commonConfigLocation, serviceConfig, serviceType)
if err != nil {
return err
}
}

overrideCount, err := cp.envVars.OverrideConfiguration(serviceConfig)
if err != nil {
return err
}
cp.lc.Infof("Common configuration loaded from file with %d overrides applied", overrideCount)
}

// Now must load configuration from local file if any of these conditions are true
// Now load the private config from a local file if any of these conditions are true
if !useProvider || !cp.providerHasConfig || cp.overwriteConfig {
// tomlTree contains the service's private configuration in its toml tree form
tomlTree, err := cp.loadPrivateFromFile()
Expand Down Expand Up @@ -251,7 +267,7 @@ func (cp *Processor) loadCommonConfig(
// check that common config is loaded into the provider
// this need a separate config provider client here because the config ready variable is stored at the common config level
// load the all services section of the common config
cp.commonConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+allServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
cp.commonConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+sep+allServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
if err != nil {
return fmt.Errorf("failed to create provider for %s: %s", allServicesKey, err.Error())
}
Expand All @@ -274,7 +290,7 @@ func (cp *Processor) loadCommonConfig(
if err != nil {
return fmt.Errorf("failed to copy the configuration structure for %s: %s", appServicesKey, err.Error())
}
cp.appConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+appServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
cp.appConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+sep+appServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
if err != nil {
return fmt.Errorf("failed to create provider for %s: %s", appServicesKey, err.Error())
}
Expand All @@ -288,7 +304,7 @@ func (cp *Processor) loadCommonConfig(
if err != nil {
return fmt.Errorf("failed to copy the configuration structure for %s: %s", deviceServicesKey, err.Error())
}
cp.deviceConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+deviceServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
cp.deviceConfigClient, err = createProvider(cp.lc, common.CoreCommonConfigServiceKey+sep+deviceServicesKey, configStem, getAccessToken, configProviderInfo.ServiceConfig())
if err != nil {
return fmt.Errorf("failed to create provider for %s: %s", deviceServicesKey, err.Error())
}
Expand All @@ -310,6 +326,53 @@ func (cp *Processor) loadCommonConfig(
return nil
}

// loadCommonConfigFromFile will pull up the common config from the provided file and load it into the passed in interface
func (cp *Processor) loadCommonConfigFromFile(
configFile string,
serviceConfig interfaces.Configuration,
serviceType string) error {

var err error

commonConfig, err := cp.loadConfigYamlFromFile(configFile)
if err != nil {
return err
}
// separate out the necessary sections
allServicesConfig, ok := commonConfig[allServicesKey].(map[string]any)
if !ok {
return fmt.Errorf("could not find %s section in common config %s", allServicesKey, configFile)
}
// use the service type to separate out the necessary sections
var serviceTypeConfig map[string]any
switch serviceType {
case config.ServiceTypeApp:
cp.lc.Infof("loading the common configuration for service type %s", serviceType)
serviceTypeConfig, ok = commonConfig[appServicesKey].(map[string]any)
if !ok {
return fmt.Errorf("could not find %s section in common config %s", appServicesKey, configFile)
}
case config.ServiceTypeDevice:
cp.lc.Infof("loading the common configuration for service type %s", serviceType)
serviceTypeConfig, ok = commonConfig[deviceServicesKey].(map[string]any)
if !ok {
return fmt.Errorf("could not find %s section in common config %s", deviceServicesKey, configFile)
}
default:
// this case is covered by the initial call to get the common config for all-services
}

if serviceType == config.ServiceTypeApp || serviceType == config.ServiceTypeDevice {
mergeMaps(allServicesConfig, serviceTypeConfig)
}

if err := convertMapToInterface(allServicesConfig, serviceConfig); err != nil {
return err
}

return err
}

func (cp *Processor) getAccessTokenCallback(serviceKey string, secretProvider interfaces.SecretProvider, err error, configProviderInfo *ProviderInfo) (types.GetAccessTokenCallback, error) {
var accessToken string
var getAccessToken types.GetAccessTokenCallback
Expand Down Expand Up @@ -527,10 +590,27 @@ func (cp *Processor) loadPrivateFromFile() (*toml.Tree, error) {
return nil, fmt.Errorf("could not convert to TOML Tree: %s", err.Error())
}

cp.lc.Info(fmt.Sprintf("Loaded private configuration from %s", filePath))
cp.lc.Infof(fmt.Sprintf("Loaded private configuration from %s", filePath))
return tomlTree, nil
}

// loadConfigYamlFromFile attempts to read the configuration yaml file
func (cp *Processor) loadConfigYamlFromFile(yamlFile string) (map[string]any, error) {
cp.lc.Infof("reading %s", yamlFile)
contents, err := os.ReadFile(yamlFile)
if err != nil {
return nil, fmt.Errorf("failed to read common configuration file %s: %s", yamlFile, err.Error())
}

data := make(map[string]any)

err = yaml.Unmarshal(contents, &data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall common configuration file %s: %s", yamlFile, err.Error())
}
return data, nil
}

func (cp *Processor) mergeTomlWithConfig(config interface{}, tomlTree *toml.Tree) error {
// convert the common config passed in to a map[string]any
var configMap map[string]any
Expand Down
49 changes: 49 additions & 0 deletions bootstrap/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"github.com/stretchr/testify/mock"
"path"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -246,6 +247,54 @@ func TestLoadCommonConfig(t *testing.T) {
}
}

func TestLoadCommonConfigFromFile(t *testing.T) {
tests := []struct {
Name string
config string
serviceConfig *ConfigurationMockStruct
serviceType string
expectedErr string
}{
{"Valid - core service", path.Join(".", "testdata", "configuration.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeOther, ""},
{"Valid - app service", path.Join(".", "testdata", "configuration.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeApp, ""},
{"Valid - device service", path.Join(".", "testdata", "configuration.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeDevice, ""},
{"Invalid - bad config file", path.Join(".", "testdata", "bad_config.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeOther, "no such file or directory"},
{"Invalid - missing all service", path.Join(".", "testdata", "bogus.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeOther, "could not find all-services section in common config"},
{"Invalid - missing app service", path.Join(".", "testdata", "all-service-config.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeApp, fmt.Sprintf("could not find %s section in common config", appServicesKey)},
{"Invalid - missing device service", path.Join(".", "testdata", "all-service-config.yaml"), &ConfigurationMockStruct{}, config.ServiceTypeDevice, fmt.Sprintf("could not find %s section in common config", deviceServicesKey)},
}

for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
// create parameters for the processor
f := flags.New()
f.Parse(nil)
mockLogger := logger.MockLogger{}
env := environment.NewVariables(mockLogger)
timer := startup.NewTimer(5, 1)
ctx, cancel := context.WithCancel(context.Background())

wg := sync.WaitGroup{}
dic := di.NewContainer(di.ServiceConstructorMap{
container.LoggingClientInterfaceName: func(get di.Get) interface{} { return mockLogger },
})
// create the processor
proc := NewProcessor(f, env, timer, ctx, &wg, nil, dic)

// call load common config
err := proc.loadCommonConfigFromFile(tc.config, tc.serviceConfig, tc.serviceType)
// make assertions
require.NotNil(t, cancel)
if tc.expectedErr == "" {
assert.NoError(t, err)
assert.NotEmpty(t, tc.serviceConfig)
return
}
assert.Contains(t, err.Error(), tc.expectedErr)
})
}
}

func TestMergeConfigs(t *testing.T) {
// create the service config
serviceConfig := ConfigurationMockStruct{
Expand Down
76 changes: 76 additions & 0 deletions bootstrap/config/testdata/all-service-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
all-services:
Writable:
InsecureSecrets:
DB:
path: "redisdb"
Secrets:
username: ""
password: ""
SecretName: "redisdb"
SecretData:
username: ""
password: ""

Telemetry:
Interval: "30s"
Metrics:
# Common Security Service Metrics
SecuritySecretsRequested: false
SecuritySecretsStored: false
SecurityConsulTokensRequested: false
SecurityConsulTokenDuration: false
Tags: # Contains the service level tags to be attached to all the service's metrics
# Gateway: "my-iot-gateway" # Tag must be added here or via Consul Env Override can only change existing value, not added new ones.

Service:
HealthCheckInterval: "10s"
Host: "localhost"
ServerBindAddr: "" # Leave blank so default to Host value unless different value is needed.
MaxResultCount: 1024
MaxRequestSize: 0 # Not currently used. Defines the maximum size of http request body in bytes
RequestTimeout: "5s"
CORSConfiguration:
EnableCORS: false
CORSAllowCredentials: false
CORSAllowedOrigin: "https://localhost"
CORSAllowedMethods: "GET, POST, PUT, PATCH, DELETE"
CORSAllowedHeaders: "Authorization, Accept, Accept-Language, Content-Language, Content-Type, X-Correlation-ID"
CORSExposeHeaders: "Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma, X-Correlation-ID"
CORSMaxAge: 3600

Registry:
Host: "localhost"
Port: 8500
Type: "consul"

Database:
Host: "localhost"
Port: 6379
Timeout: 5000
Type: "redisdb"

MessageBus:
Protocol: "redis"
Host: "localhost"
Port: 6379
Type: "redis"
AuthMode: "usernamepassword" # required for redis MessageBus (secure or insecure).
SecretName: "redisdb"
BaseTopicPrefix: "edgex" # prepended to all topics as "edgex/<additional topic levels>
Optional:
# Default MQTT Specific options that need to be here to enable environment variable overrides of them
Qos: "0" # Quality of Service values are 0 (At most once), 1 (At least once) or 2 (Exactly once)
KeepAlive: "10" # Seconds (must be 2 or greater)
Retained: "false"
AutoReconnect: "true"
ConnectTimeout: "5" # Seconds
SkipCertVerify: "false"
# Additional Default NATS Specific options that need to be here to enable environment variable overrides of them
Format: "nats"
RetryOnFailedConnect: "true"
QueueGroup: ""
Durable: ""
AutoProvision: "true"
Deliver: "new"
DefaultPubRetryAttempts: "2"
Subject: "edgex/#" # Required for NATS JetStream only for stream auto-provisioning
Empty file.
Loading

0 comments on commit fed18d9

Please sign in to comment.