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: added REST API for validating yaml VPP-Agent configuration using validate methods on registered descriptors #1786

Merged
merged 8 commits into from
Mar 3, 2021
43 changes: 31 additions & 12 deletions client/dynamic_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func createDynamicConfigDescriptorProto(knownModels []*ModelInfo, dependencyRegi
importedDependency := make(map[string]struct{}) // just for deduplication checking
for _, modelDetail := range knownModels {
// get/create group config for this know model (all configs are grouped into groups based on their module)
configGroupName := fmt.Sprintf("%v%v", modulePrefix(models.ToSpec(modelDetail.Spec).ModelName()), configGroupSuffix)
configGroupName := DynamicConfigGroupFieldNaming(modelDetail)
configGroup, found := configGroups[configGroupName]
if !found { // create it!
// create new message that represents new config group
Expand All @@ -186,21 +186,11 @@ func createDynamicConfigDescriptorProto(knownModels []*ModelInfo, dependencyRegi
}

// fill config group message with currently handled known model
simpleProtoName := simpleProtoName(modelDetail.ProtoName)
protoName := string(simpleProtoName) + repeatedFieldsSuffix
jsonName := string(simpleProtoName) + repeatedFieldsSuffix
label := protoLabel(descriptorpb.FieldDescriptorProto_LABEL_REPEATED)
if !existsModelOptionFor("nameTemplate", modelDetail.Options) {
label = protoLabel(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL)
protoName = string(simpleProtoName)
jsonName = string(simpleProtoName)
}
compatibilityKey := fmt.Sprintf("%v.%v", configGroupName, string(simpleProtoName))
if newNames, found := backwardCompatibleNames[compatibilityKey]; found {
// using field names from hardcoded configurator.Config to achieve json/yaml backward compatibility
protoName = newNames.protoName
jsonName = newNames.jsonName
}
protoName, jsonName := DynamicConfigKnownModelFieldNaming(modelDetail)
configGroup.Field = append(configGroup.Field, &descriptorpb.FieldDescriptorProto{
Name: proto.String(protoName),
Number: proto.Int32(int32(len(configGroup.Field) + 1)),
Expand Down Expand Up @@ -240,6 +230,35 @@ func createDynamicConfigDescriptorProto(knownModels []*ModelInfo, dependencyRegi
return
}

// DynamicConfigGroupFieldNaming computes for given known model the naming of configuration group proto field
// containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration.
// The json name of the field is the same as proto name of field.
func DynamicConfigGroupFieldNaming(modelDetail *models.ModelInfo) string {
return fmt.Sprintf("%v%v", modulePrefix(models.ToSpec(modelDetail.Spec).ModelName()), configGroupSuffix)
}

// DynamicConfigKnownModelFieldNaming compute for given known model the (proto and json) naming of proto field
// containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration.
func DynamicConfigKnownModelFieldNaming(modelDetail *models.ModelInfo) (protoName, jsonName string) {
simpleProtoName := simpleProtoName(modelDetail.ProtoName)
configGroupName := DynamicConfigGroupFieldNaming(modelDetail)
compatibilityKey := fmt.Sprintf("%v.%v", configGroupName, simpleProtoName)

if newNames, found := backwardCompatibleNames[compatibilityKey]; found {
// using field names from hardcoded configurator.Config to achieve json/yaml backward compatibility
protoName = newNames.protoName
jsonName = newNames.jsonName
} else if !existsModelOptionFor("nameTemplate", modelDetail.Options) {
protoName = simpleProtoName
jsonName = simpleProtoName
} else {
protoName = simpleProtoName + repeatedFieldsSuffix
jsonName = simpleProtoName + repeatedFieldsSuffix
}

return protoName, jsonName
}

// DynamicConfigExport exports from dynamic config the proto.Messages corresponding to known models that
// were given as input when dynamic config was created using NewDynamicConfig. This is a convenient
// method how to extract data for generic client usage (proto.Message instances) from value-filled
Expand Down
8 changes: 8 additions & 0 deletions pkg/models/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package models

import (
"reflect"

"github.com/golang/protobuf/proto"
"go.ligato.io/vpp-agent/v3/proto/ligato/generic"
"google.golang.org/protobuf/reflect/protoreflect"
Expand Down Expand Up @@ -78,6 +80,12 @@ type KnownModel interface {
// GoType returns go type for the model.
GoType() string

// LocalGoType returns reflect go type for the model. The reflect type can be retrieved only
// for locally registered model that provide locally known go types. The remotely retrieved model
// can't provide reflect type so if known model information is retrieved remotely, this method
// will return nil.
LocalGoType() reflect.Type

// PkgPath returns package import path for the model definition.
PkgPath() string

Expand Down
5 changes: 5 additions & 0 deletions pkg/models/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ func (m *LocallyKnownModel) GoType() string {
return m.goType.String()
}

// LocalGoType returns reflect go type for the model.
func (m *LocallyKnownModel) LocalGoType() reflect.Type {
return m.goType
}

// PkgPath returns package import path for the model definition.
func (m *LocallyKnownModel) PkgPath() string {
return m.goType.Elem().PkgPath()
Expand Down
4 changes: 2 additions & 2 deletions pkg/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ func keyPrefix(modelSpec Spec, hasTemplateName bool) string {
return keyPrefix
}

// dynamicMessageToGeneratedMessage converts proto dynamic message to corresponding generated proto message
// DynamicMessageToGeneratedMessage converts proto dynamic message to corresponding generated proto message
// (identified by go type).
// This conversion method should help handling dynamic proto messages in mostly protoc-generated proto message
// oriented codebase (i.e. help for type conversions to named, help handle missing data fields as seen
// in generated proto messages,...)
func dynamicMessageToGeneratedMessage(dynamicMessage *dynamicpb.Message,
func DynamicMessageToGeneratedMessage(dynamicMessage *dynamicpb.Message,
goTypeOfGeneratedMessage reflect.Type) (proto.Message, error) {

// create empty proto message of the same type as it was used for registration
Expand Down
2 changes: 1 addition & 1 deletion pkg/models/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NameTemplate(t string) NameFunc {
// handling dynamic messages (they don't have data fields as generated proto messages)
if dynMessage, ok := obj.(*dynamicpb.Message); ok {
var err error
obj, err = dynamicMessageToGeneratedMessage(dynMessage, messageGoType)
obj, err = DynamicMessageToGeneratedMessage(dynMessage, messageGoType)
if err != nil {
return "", err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/models/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (r *LocalRegistry) Register(x interface{}, spec Spec, opts ...ModelOption)
model.nameFunc = func(obj interface{}, messageGoType reflect.Type) (s string, e error) {
// handling dynamic messages (they don't implement named interface)
if dynMessage, ok := obj.(*dynamicpb.Message); ok {
obj, e = dynamicMessageToGeneratedMessage(dynMessage, messageGoType)
obj, e = DynamicMessageToGeneratedMessage(dynMessage, messageGoType)
if e != nil {
return "", e
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/models/remote_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package models

import (
"encoding/json"
"reflect"
"strings"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -78,6 +79,12 @@ func (m *RemotelyKnownModel) GoType() string {
return goType
}

// LocalGoType should returns reflect go type for the model, but remotely known model doesn't have
// locally known reflect go type. It always returns nil.
func (m *RemotelyKnownModel) LocalGoType() reflect.Type {
return nil
}

// PkgPath returns package import path for the model definition.
func (m *RemotelyKnownModel) PkgPath() string {
pkgPath, _ := m.modelOptionFor("pkgPath", m.model.Options)
Expand Down Expand Up @@ -176,7 +183,7 @@ func (m *RemotelyKnownModel) replaceFieldNamesInNameTemplate(messageDesc protore
for i := 0; i < messageDesc.Fields().Len(); i++ {
fieldDesc := messageDesc.Fields().Get(i)
pbJSONName := fieldDesc.JSONName()
nameTemplate = strings.ReplaceAll(nameTemplate, "." + m.upperFirst(pbJSONName), "." + pbJSONName)
nameTemplate = strings.ReplaceAll(nameTemplate, "."+m.upperFirst(pbJSONName), "."+pbJSONName)
if fieldDesc.Message() != nil {
nameTemplate = m.replaceFieldNamesInNameTemplate(fieldDesc.Message(), nameTemplate)
}
Expand Down
81 changes: 81 additions & 0 deletions plugins/kvscheduler/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,87 @@ func (e *InvalidValueError) GetInvalidFields() []string {
return e.invalidFields
}

/***************************** Invalid Message ****************************/

// InvalidMessageError is message validation error that links proto message with its
// corresponding InvalidValueError returned from running KVDescriptor.Validate on the given proto message
type InvalidMessageError struct {
// message is the message to which the invalidError refers to
message proto.Message
// parentMessage is filled only when field message is created using KVDescriptor.DerivedValues and
// it holds the proto message corresponding to the KVDescriptor that was used the derive value.
parentMessage proto.Message
// invalidError is the field validation error from KVDescriptor.Validate()
invalidError *InvalidValueError
}

// NewInvalidMessageError is constructor for InvalidMessageError
func NewInvalidMessageError(message proto.Message, invalidError *InvalidValueError,
parentMessage proto.Message) *InvalidMessageError {
return &InvalidMessageError{
message: message,
parentMessage: parentMessage,
invalidError: invalidError,
}
}

// Error returns string representation of the pair (proto message, its InvalidValueError)
func (e *InvalidMessageError) Error() string {
return fmt.Sprintf("message is not valid due to: %v (message=%v, parentMessage=%v)",
e.invalidError.Error(), e.message, e.parentMessage)
}

// Message returns proto message to which the InvalidValueError is linked
func (e *InvalidMessageError) Message() proto.Message {
return e.message
}

// InvalidFields return fields to which the InvalidValueError is referring
func (e *InvalidMessageError) InvalidFields() []string {
return e.invalidError.GetInvalidFields()
}

// ValidationError return error message of linked InvalidValueError
func (e *InvalidMessageError) ValidationError() error {
return e.invalidError.GetValidationError()
}

// ParentMessage returns parent proto message to message which the InvalidValueError is linked to. The parent
// proto message is non-nill only when the KVDescriptor.DerivedValues was used to create message
// (InvalidMessageError.Message()), otherwise it is nil
func (e *InvalidMessageError) ParentMessage() proto.Message {
return e.parentMessage
}

/***************************** Invalid Messages ****************************/

// InvalidMessagesError is container for multiple InvalidMessageError instances
type InvalidMessagesError struct {
messageErrors []*InvalidMessageError
}

// NewInvalidMessagesError is constructor for new InvalidMessagesError instances
func NewInvalidMessagesError(messageErrors []*InvalidMessageError) *InvalidMessagesError {
return &InvalidMessagesError{
messageErrors: messageErrors,
}
}

// Error returns string representation of all contained InvalidMessageError instances
func (e *InvalidMessagesError) Error() string {
var sb strings.Builder
sb.WriteString("some messages are invalid:\n")
for _, me := range e.messageErrors {
sb.WriteString(me.Error() + "\n")
}
return sb.String()
}

// MessageErrors returns all InvalidMessageError instances
func (e *InvalidMessagesError) MessageErrors() []*InvalidMessageError {
return e.messageErrors
}

/***************************** Verification Failure ****************************/

type VerificationErrorType int
Expand Down
10 changes: 10 additions & 0 deletions plugins/kvscheduler/api/kv_scheduler_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,16 @@ type KVScheduler interface {
// GetRecordedTransaction returns record of a transaction referenced
// by the sequence number.
GetRecordedTransaction(SeqNum uint64) (txn *RecordedTxn)

// ValidateSemantically validates given proto messages according to semantic validation(KVDescriptor.Validate)
// from registered KVDescriptors. If all locally known messages are valid, nil is returned. If some locally known
// messages are invalid, kvscheduler.MessageValidationErrors is returned. In any other case, error is returned.
//
// Usage of dynamic proto messages (dynamicpb.Message) described by remotely known models is not supported.
// The reason for this is that the KVDescriptors can validate only statically generated proto messages and
// remotely retrieved dynamic proto messages can't be converted to such proto messages (there are
// no locally available statically generated proto models).
ValidateSemantically([]proto.Message) error
}

// ValueProvider provides key/value data from different sources in system (NB, SB, KVProvider cache of SB)
Expand Down
2 changes: 1 addition & 1 deletion plugins/kvscheduler/plugin_scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ package kvscheduler

import (
"context"
"errors"
"os"
"runtime/trace"
"sync"
"time"

"github.com/go-errors/errors"
"github.com/golang/protobuf/proto"

"go.ligato.io/cn-infra/v2/idxmap"
Expand Down
Loading