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 getting and setting NB VPP-Agent configuration #1787

Merged
merged 6 commits into from
Mar 10, 2021
56 changes: 50 additions & 6 deletions client/local_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,31 @@ import (
"go.ligato.io/cn-infra/v2/datasync/kvdbsync/local"
"go.ligato.io/cn-infra/v2/datasync/syncbase"
"go.ligato.io/cn-infra/v2/db/keyval"

"go.ligato.io/vpp-agent/v3/pkg/models"
"go.ligato.io/vpp-agent/v3/pkg/util"
"go.ligato.io/vpp-agent/v3/plugins/orchestrator"
"go.ligato.io/vpp-agent/v3/plugins/orchestrator/contextdecorator"
"go.ligato.io/vpp-agent/v3/proto/ligato/generic"
protoV2 "google.golang.org/protobuf/proto"
)

// LocalClient is global client for direct local access.
var LocalClient = NewClient(&txnFactory{local.DefaultRegistry})
// Updates and resyncs of this client use local.DefaultRegistry for propagating data to orchestrator.Dispatcher
// (going through watcher.Aggregator together with other data sources). However, data retrieval uses
// orchestrator.Dispatcher directly.
var LocalClient = NewClient(&txnFactory{local.DefaultRegistry}, &orchestrator.DefaultPlugin)

type client struct {
txnFactory ProtoTxnFactory
dispatcher orchestrator.Dispatcher
}

// NewClient returns new instance that uses given registry for data propagation.
func NewClient(factory ProtoTxnFactory) ConfigClient {
return &client{factory}
// NewClient returns new instance that uses given registry for data propagation and dispatcher for data retrieval.
func NewClient(factory ProtoTxnFactory, dispatcher orchestrator.Dispatcher) ConfigClient {
return &client{
txnFactory: factory,
dispatcher: dispatcher,
}
}

func (c *client) KnownModels(class string) ([]*ModelInfo, error) {
Expand Down Expand Up @@ -69,7 +78,16 @@ func (c *client) ResyncConfig(items ...proto.Message) error {
}

func (c *client) GetConfig(dsts ...interface{}) error {
// TODO: use dispatcher to get config
protos := c.dispatcher.ListData()
protoDsts := extractProtoMessages(dsts)
if len(dsts) == len(protoDsts) { // all dsts are proto messages
// TODO the clearIgnoreLayerCount function argument should be a option of generic.Client
// (the value 1 generates from dynamic config the same json/yaml output as the hardcoded
// configurator.Config and therefore serves for backward compatibility)
util.PlaceProtosIntoProtos(convertToProtoV2(protos), 1, protoDsts...)
} else {
util.PlaceProtos(protos, dsts...)
}
return nil
}

Expand Down Expand Up @@ -143,3 +161,29 @@ func (p *txnFactory) NewTxn(resync bool) keyval.ProtoTxn {
}
return local.NewProtoTxn(p.registry.PropagateChanges)
}

func extractProtoMessages(dsts []interface{}) []protoV2.Message {
protoDsts := make([]protoV2.Message, 0)
for _, dst := range dsts {
protoV1Dst, isProtoV1 := dst.(proto.Message)
if isProtoV1 {
protoDsts = append(protoDsts, proto.MessageV2(protoV1Dst))
} else {
protoV2Dst, isProtoV2 := dst.(protoV2.Message)
if isProtoV2 {
protoDsts = append(protoDsts, protoV2Dst)
} else {
break
}
}
}
return protoDsts
}

func convertToProtoV2(protoMap map[string]proto.Message) []protoV2.Message {
result := make([]protoV2.Message, 0, len(protoMap))
for _, msg := range protoMap {
result = append(result, proto.MessageV2(msg))
}
return result
}
2 changes: 1 addition & 1 deletion pkg/models/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,5 @@ func (m *LocallyKnownModel) InstanceName(x interface{}) (string, error) {
if m.nameFunc == nil {
return "", nil
}
return m.nameFunc(x, m.goType)
return m.nameFunc(x)
}
29 changes: 20 additions & 9 deletions pkg/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,31 @@ func keyPrefix(modelSpec Spec, hasTemplateName bool) string {
return keyPrefix
}

// DynamicMessageToGeneratedMessage converts proto dynamic message to corresponding generated proto message
// (identified by go type).
// DynamicLocallyKnownMessageToGeneratedMessage converts locally registered/known proto dynamic message to
// corresponding statically generated proto message. This function will fail when there is no registration
// of statically-generated proto message, i.e. dynamic message refers to remotely known model.
// 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,
goTypeOfGeneratedMessage reflect.Type) (proto.Message, error) {
func DynamicLocallyKnownMessageToGeneratedMessage(dynamicMessage *dynamicpb.Message) (proto.Message, error) {
// get go type of statically generated proto message corresponding to locally known dynamic message
model, err := GetModelFor(dynamicMessage)
if err != nil {
return nil, errors.Errorf("can't get model "+
"for dynamic message due to: %v (message=%v)", err, dynamicMessage)
}
goType := model.LocalGoType() // only for locally known models will return meaningful go type
if goType == nil {
return nil, errors.Errorf("dynamic messages for remote models are not supported due to "+
"not available go type of statically generated proto message (dynamic message=%v)", dynamicMessage)
}

// create empty proto message of the same type as it was used for registration
// create empty statically-generated proto message of the same type as it was used for registration
var registeredGoType interface{}
if goTypeOfGeneratedMessage.Kind() == reflect.Ptr {
registeredGoType = reflect.New(goTypeOfGeneratedMessage.Elem()).Interface()
if goType.Kind() == reflect.Ptr {
registeredGoType = reflect.New(goType.Elem()).Interface()
} else {
registeredGoType = reflect.Zero(goTypeOfGeneratedMessage).Interface()
registeredGoType = reflect.Zero(goType).Interface()
}
message, isProtoV1 := registeredGoType.(proto.Message)
if !isProtoV1 {
Expand All @@ -164,7 +175,7 @@ func DynamicMessageToGeneratedMessage(dynamicMessage *dynamicpb.Message,
message = proto.MessageV1(messageV2)
}

// fill empty proto message with data from its dynamic proto message counterpart
// fill empty statically-generated proto message with data from its dynamic proto message counterpart
// (alternative approach to this is marshalling dynamicMessage to json and unmarshalling it back to message)
proto.Merge(message, dynamicMessage)

Expand Down
14 changes: 6 additions & 8 deletions pkg/models/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package models

import (
"net"
"reflect"
"strings"
"text/template"

Expand All @@ -18,10 +17,7 @@ type modelOptions struct {
type ModelOption func(*modelOptions)

// NameFunc represents function which can name model instance.
// To properly handle also dynamic Messages (dynamicpb.Message)
// as model instances, the go type of corresponding generated
// proto message must be given.
type NameFunc func(obj interface{}, messageGoType reflect.Type) (string, error)
type NameFunc func(obj interface{}) (string, error)

// WithNameTemplate returns option for models which sets function
// for generating name of instances using custom template.
Expand All @@ -42,11 +38,13 @@ func NameTemplate(t string) NameFunc {
tmpl := template.Must(
template.New("name").Funcs(funcMap).Option("missingkey=error").Parse(t),
)
return func(obj interface{}, messageGoType reflect.Type) (string, error) {
// handling dynamic messages (they don't have data fields as generated proto messages)
return func(obj interface{}) (string, error) {
// handling locally known dynamic messages (they don't have data fields as generated proto messages)
// (dynamic messages of remotely known models are not supported, remote_model implementation is
// not using dynamic message for name template resolving so it is ok)
if dynMessage, ok := obj.(*dynamicpb.Message); ok {
var err error
obj, err = DynamicMessageToGeneratedMessage(dynMessage, messageGoType)
obj, err = DynamicLocallyKnownMessageToGeneratedMessage(dynMessage)
if err != nil {
return "", err
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/models/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ func (r *LocalRegistry) Register(x interface{}, spec Spec, opts ...ModelOption)

// Use GetName as fallback for generating name
if _, ok := x.(named); ok {
model.nameFunc = func(obj interface{}, messageGoType reflect.Type) (s string, e error) {
model.nameFunc = func(obj interface{}) (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 = DynamicLocallyKnownMessageToGeneratedMessage(dynMessage)
if e != nil {
return "", e
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/models/remote_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func (m *RemotelyKnownModel) InstanceName(x interface{}) (string, error) {
return "", errors.Errorf("can't load json of marshalled "+
"message to generic map due to: %v (json=%v)", err, jsonData)
}
name, err := NameTemplate(nameTemplate)(mapData, nil)
name, err := NameTemplate(nameTemplate)(mapData)
if err != nil {
return "", errors.Errorf("can't compute name from name template by applying generic map "+
"due to: %v (name template=%v, generic map=%v)", err, nameTemplate, mapData)
Expand Down
51 changes: 30 additions & 21 deletions pkg/util/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
package util

import (
"reflect"

protoV2 "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"reflect"

"github.com/golang/protobuf/proto"
)
Expand Down Expand Up @@ -82,42 +83,50 @@ func PlaceProtos(protos map[string]proto.Message, dsts ...interface{}) {
// how many top model structure hierarchy layers can have empty values for messages (see
// util.placeProtosInProto(...) for details)
func PlaceProtosIntoProtos(protos []protoV2.Message, clearIgnoreLayerCount int, dsts ...protoV2.Message) {
protosMap := make(map[string][]protoV2.Message)
// create help structure for insertion proto messages
// (map values are protoreflect.Message(s) that contains proto message and its type. These messages will be
// later wrapped into protoreflect.Value(s) and filled into destination proto message using proto reflection.
// We could have used protoreflect.Value(s) as map values in this help structure, but protoreflect.Value type
// is cheap (really thin wrapper) and it is unknown whether using the same value on multiple fields could
// cause problems, so we will generate it for each field.
messageMap := make(map[string][]protoreflect.Message)
for _, protoMsg := range protos {
protoName := string(protoMsg.ProtoReflect().Descriptor().FullName())
protosMap[protoName] = append(protosMap[protoName], protoMsg)
messageMap[protoName] = append(messageMap[protoName], protoMsg.ProtoReflect())
}

// insert proto message to all destination containers (also proto messages)
for _, dst := range dsts {
placeProtosInProto(dst, protosMap, clearIgnoreLayerCount)
placeProtosInProto(dst, messageMap, clearIgnoreLayerCount)
}
}

// placeProtosInProto fills dst proto message (direct or transitive) fields with protos values from protoMap
// (convenient map[proto descriptor full name]= proto value). The matching is done by message descriptor's
// full name. The function is recursive and one run is handling only one level of proto message structure tree
// (only handling Message references and ignoring scalar/enum/... values). The <clearIgnoreLayerCount> controls
// how many top layer can have empty values for their message fields (as the algorithm backtracks the descriptor
// model tree, it unfortunately initialize empty value for visited fields). The layer below
// <clearIgnoreLayerCount> top layer will be cleared from the fake empty value.
// Currently unsupported are maps as fields.
func placeProtosInProto(dst protoV2.Message, protosMap map[string][]protoV2.Message, clearIgnoreLayerCount int) bool {
// placeProtosInProto fills dst proto message (direct or transitive) fields with protos values from messageMap
// (convenient map[proto descriptor full name]= protoreflect message containing proto message and proto type).
// The matching is done by message descriptor's full name. The function is recursive and one run is handling
// only one level of proto message structure tree (only handling Message references and ignoring
// scalar/enum/... values). The <clearIgnoreLayerCount> controls how many top layer can have empty values
// for their message fields (as the algorithm backtracks the descriptor model tree, it unfortunately initialize
// empty value for visited fields). The layer below <clearIgnoreLayerCount> top layer will be cleared
// from the fake empty value. Currently unsupported are maps as fields.
func placeProtosInProto(dst protoV2.Message, messageMap map[string][]protoreflect.Message, clearIgnoreLayerCount int) bool {
changed := false
fields := dst.ProtoReflect().Descriptor().Fields()
for i := 0; i < fields.Len(); i++ {
field := fields.Get(i)
fieldMessageDesc := field.Message()
if fieldMessageDesc != nil { // only interested in MessageKind or GroupKind fields
if protoMsgsForField, typeMatch := protosMap[string(fieldMessageDesc.FullName())]; typeMatch {
if messageForField, typeMatch := messageMap[string(fieldMessageDesc.FullName())]; typeMatch {
// fill value(s)
if field.IsList() {
list := dst.ProtoReflect().Mutable(field).List()
for _, protoMsg := range protoMsgsForField {
list.Append(protoreflect.ValueOf(protoMsg))
for _, message := range messageForField {
list.Append(protoreflect.ValueOf(message))
changed = true
}
} else if field.IsMap() { // unsupported
} else {
dst.ProtoReflect().Set(field, protoreflect.ValueOf(protoMsgsForField[0]))
dst.ProtoReflect().Set(field, protoreflect.ValueOf(messageForField[0]))
changed = true
}
} else {
Expand All @@ -129,9 +138,9 @@ func placeProtosInProto(dst protoV2.Message, protosMap map[string][]protoV2.Mess
if field.IsList() {
list := dst.ProtoReflect().Mutable(field).List()
changeOnLowerLayer := false
for j:=0; j < list.Len(); j++ {
for j := 0; j < list.Len(); j++ {
changeOnLowerLayer = changeOnLowerLayer ||
placeProtosInProto(list.Get(j).Message().Interface(), protosMap, clearIgnoreLayerCount- 1)
placeProtosInProto(list.Get(j).Message().Interface(), messageMap, clearIgnoreLayerCount-1)
}
if !changeOnLowerLayer && clearIgnoreLayerCount <= 0 {
dst.ProtoReflect().Clear(field)
Expand All @@ -140,7 +149,7 @@ func placeProtosInProto(dst protoV2.Message, protosMap map[string][]protoV2.Mess
} else if field.IsMap() { // unsupported
} else {
changeOnLowerLayer := placeProtosInProto(dst.ProtoReflect().Mutable(field).
Message().Interface(), protosMap, clearIgnoreLayerCount- 1)
Message().Interface(), messageMap, clearIgnoreLayerCount-1)
if !changeOnLowerLayer && clearIgnoreLayerCount <= 0 {
dst.ProtoReflect().Clear(field)
}
Expand All @@ -150,4 +159,4 @@ func placeProtosInProto(dst protoV2.Message, protosMap map[string][]protoV2.Mess
}
}
return changed
}
}
2 changes: 1 addition & 1 deletion plugins/kvscheduler/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (s *Scheduler) ValidateSemantically(messages []proto.Message) error {
"using KVDescriptor.Validate (dynamic message=%v)", model.ProtoName(), message)
continue
}
message, err = models.DynamicMessageToGeneratedMessage(dynamicMessage, goType)
message, err = models.DynamicLocallyKnownMessageToGeneratedMessage(dynamicMessage)
if err != nil {
return errors.Errorf("can't convert dynamic message to statically generated message "+
"due to: %v (dynamic message=%v)", err, dynamicMessage)
Expand Down
3 changes: 2 additions & 1 deletion plugins/orchestrator/localregistry/initfileregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"go.ligato.io/cn-infra/v2/db/keyval"
"go.ligato.io/cn-infra/v2/infra"
"go.ligato.io/vpp-agent/v3/client"
"go.ligato.io/vpp-agent/v3/plugins/orchestrator"
"google.golang.org/protobuf/encoding/protojson"
protoV2 "google.golang.org/protobuf/proto"
)
Expand Down Expand Up @@ -198,7 +199,7 @@ func (r *InitFileRegistry) watchResync(resyncReg resync.Registration) {
// resyncReg.StatusChan == Started => resync
if resyncStatus.ResyncStatus() == resync.Started && !r.pushedToWatchedRegistry {
if !r.Empty() { // put preloaded NB init file data into watched p.registry
c := client.NewClient(&txnFactory{r.watchedRegistry})
c := client.NewClient(&txnFactory{r.watchedRegistry}, &orchestrator.DefaultPlugin)
if err := c.ResyncConfig(r.preloadedNBConfigs...); err != nil {
r.Log.Errorf("resyncing preloaded NB init file data "+
"into watched registry failed: %v", err)
Expand Down
Loading