Skip to content

Commit

Permalink
feat: added REST API for getting and setting NB VPP-Agent configurati…
Browse files Browse the repository at this point in the history
…on (#1787)

* feat: implemented Get capabilities of local client

Signed-off-by: Filip Gschwandtner <[email protected]>

* fix: fix for statically-generated proto messages using proto message related utils (bad type assumption that luckily worked for dynamic message but not for statically-genereated messages)

Signed-off-by: Filip Gschwandtner <[email protected]>

* refactor: moved go type retrieval of statically-generated proto message into conversion function (dynamic to staticly-generated message) that needs it (this simplifies function usage and related nameFunc API)

Signed-off-by: Filip Gschwandtner <[email protected]>

* fix: fixed local client usage for previous api changes

Signed-off-by: Filip Gschwandtner <[email protected]>

* feat: added PUT and GET of NB VPP-Agent configuration to REST API

Signed-off-by: Filip Gschwandtner <[email protected]>

* fix: added different handling for proxy remote modeled proto messages in PUT NB configuration in REST API

Signed-off-by: Filip Gschwandtner <[email protected]>
  • Loading branch information
fgschwan authored Mar 10, 2021
1 parent fd54fd6 commit 353481d
Show file tree
Hide file tree
Showing 13 changed files with 361 additions and 101 deletions.
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

0 comments on commit 353481d

Please sign in to comment.