Skip to content

Commit

Permalink
Add recursive validation to config
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsains committed Nov 18, 2022
1 parent 4565692 commit e9d7a1b
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 0 deletions.
74 changes: 74 additions & 0 deletions service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package service // import "go.opentelemetry.io/collector/service"
import (
"errors"
"fmt"
"reflect"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config"
Expand Down Expand Up @@ -76,25 +77,98 @@ func (cfg *Config) Validate() error {
if err := component.ValidateConfig(expCfg); err != nil {
return fmt.Errorf("exporter %q has invalid configuration: %w", expID, err)
}
if structName, err := callAllValidateMethods(reflect.ValueOf(expCfg)); err != nil {
return fmt.Errorf("exporter %q has invalid configuration in %q: %w", expID, structName, err)
}
}

// Validate the processor configuration.
for procID, procCfg := range cfg.Processors {
if err := component.ValidateConfig(procCfg); err != nil {
return fmt.Errorf("processor %q has invalid configuration: %w", procID, err)
}
if structName, err := callAllValidateMethods(reflect.ValueOf(procCfg)); err != nil {
return fmt.Errorf("processor %q has invalid configuration in %q: %w", procID, structName, err)
}
}

// Validate the extension configuration.
for extID, extCfg := range cfg.Extensions {
if err := component.ValidateConfig(extCfg); err != nil {
return fmt.Errorf("extension %q has invalid configuration: %w", extID, err)
}
if structName, err := callAllValidateMethods(reflect.ValueOf(extCfg)); err != nil {
return fmt.Errorf("extension %q has invalid configuration in %q: %w", extID, structName, err)
}
}

return cfg.validateService()
}

// Go through each nested structure in the given structure and call its Validate method.
// the Validate method of r has already been checked, only need to check its children
func callAllValidateMethods(r reflect.Value) (string, error) {
invokeValidate := func(r reflect.Value) error {
if _, ok := r.Interface().(component.ConfigValidator); ok {
output := r.MethodByName("Validate").Call([]reflect.Value{})
if len(output) > 0 {
returnVal := output[0].Interface()
if returnVal != nil {
return returnVal.(error)
}
}
}
return nil
}

joinPathElements := func(head, rest string) string {
switch {
case rest == "":
return head
case rest[0:1] == "[":
return head + rest
default:
return head + "." + rest
}
}

switch r.Kind() {
case reflect.Ptr:
// if it's a pointer, follow to its value
return callAllValidateMethods(r.Elem())
case reflect.Struct:
// If it's a struct, first check if it already has a Validate method.
// If it fails, not need to check further
// TODO: if a Validate method exists and returns nil, should we refrain from checking its children?
if r.CanAddr() {
rp := r.Addr()
if rp.CanInterface() {
err := invokeValidate(rp)
if err != nil {
return "", err
}
}
}
// Next, recurse into the struct's fields to check validity
for i := 0; i < r.Type().NumField(); i++ {
structField := r.Type().Field(i)
if childName, err := callAllValidateMethods(r.Field(i)); err != nil {
return joinPathElements(structField.Name, childName), err
}
}

case reflect.Slice, reflect.Array:
// If it's an array, simply iterate through the elements looking for an error
for i := 0; i < r.Len(); i++ {
elem := r.Index(i)
if childName, err := callAllValidateMethods(elem); err != nil {
return joinPathElements(fmt.Sprintf("[%d]", i), childName), err
}
}
}
return "", nil
}

func (cfg *Config) validateService() error {
// Check that all enabled extensions in the service are configured.
for _, ref := range cfg.Service.Extensions {
Expand Down
40 changes: 40 additions & 0 deletions service/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ type nopExtConfig struct {
validateErr error
}

type nopExtConfigWithChildStruct struct {
config.ExtensionSettings
ChildWithError nopExtConfig
}

type nopExtConfigWithChildArray struct {
config.ExtensionSettings
ChildWithError []nopExtConfig
}

func (nc *nopExtConfig) Validate() error {
return nc.validateErr
}
Expand Down Expand Up @@ -235,6 +245,36 @@ func TestConfigValidate(t *testing.T) {
},
expected: fmt.Errorf(`extension "nop" has invalid configuration: %w`, errInvalidExtConfig),
},
{
name: "invalid-struct-children-of-extension-config",
cfgFn: func() *Config {
cfg := generateConfig()
cfg.Extensions[component.NewID("nop")] = &nopExtConfigWithChildStruct{
ExtensionSettings: config.NewExtensionSettings(component.NewID("nop")),
ChildWithError: nopExtConfig{
validateErr: errInvalidExtConfig,
},
}
return cfg
},
expected: fmt.Errorf(`extension "nop" has invalid configuration in "ChildWithError": %w`, errInvalidExtConfig),
},
{
name: "invalid-array-children-of-extension-config",
cfgFn: func() *Config {
cfg := generateConfig()
cfg.Extensions[component.NewID("nop")] = &nopExtConfigWithChildArray{
ExtensionSettings: config.NewExtensionSettings(component.NewID("nop")),
ChildWithError: []nopExtConfig{
{
validateErr: errInvalidExtConfig,
},
},
}
return cfg
},
expected: fmt.Errorf(`extension "nop" has invalid configuration in "ChildWithError[0]": %w`, errInvalidExtConfig),
},
{
name: "invalid-service-pipeline-type",
cfgFn: func() *Config {
Expand Down

0 comments on commit e9d7a1b

Please sign in to comment.