go.ligato.io/vpp-agent/v3@v3.5.0/client/dynamic_config.go (about)

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/goccy/go-yaml"
     8  	"go.ligato.io/cn-infra/v2/logging/logrus"
     9  	"google.golang.org/protobuf/encoding/protojson"
    10  	"google.golang.org/protobuf/proto"
    11  	"google.golang.org/protobuf/reflect/protodesc"
    12  	"google.golang.org/protobuf/reflect/protoreflect"
    13  	"google.golang.org/protobuf/reflect/protoregistry"
    14  	"google.golang.org/protobuf/types/descriptorpb"
    15  	"google.golang.org/protobuf/types/dynamicpb"
    16  
    17  	"go.ligato.io/vpp-agent/v3/pkg/models"
    18  	"go.ligato.io/vpp-agent/v3/pkg/util"
    19  	"go.ligato.io/vpp-agent/v3/proto/ligato/generic"
    20  )
    21  
    22  // field proto name/json name constants (can't be changes to not break json/yaml compatibility with configurator.Config)
    23  const (
    24  	// configGroupSuffix is field proto name suffix that all fields referencing config groups has
    25  	configGroupSuffix = "Config"
    26  	// repeatedFieldsSuffix is suffix added to repeated fields inside config group message
    27  	repeatedFieldsSuffix = "_list"
    28  )
    29  
    30  // names is supporting structure for remembering proto field name and json name
    31  type names struct {
    32  	protoName, jsonName string
    33  }
    34  
    35  // TODO: generate backwardCompatibleNames dynamically by searching given known model in configurator.Config
    36  //  and extracting proto field name and json name?
    37  
    38  // backwardCompatibleNames is mappging from dynamic Config fields (derived from currently known models) to
    39  // hardcoded names (proto field name/json name) in hardcoded configurator.Config. This mapping should allow
    40  // dynamically-created Config to read/write configuration from/to json/yaml files in the same way as it is
    41  // for hardcoded configurator.Config.
    42  var backwardCompatibleNames = map[string]names{
    43  	"netallocConfig.IPAllocation":      names{protoName: "ip_addresses", jsonName: "ipAddresses"},
    44  	"linuxConfig.Interface":            names{protoName: "interfaces", jsonName: "interfaces"},
    45  	"linuxConfig.ARPEntry":             names{protoName: "arp_entries", jsonName: "arpEntries"},
    46  	"linuxConfig.Route":                names{protoName: "routes", jsonName: "routes"},
    47  	"linuxConfig.RuleChain":            names{protoName: "RuleChain", jsonName: "RuleChain"},
    48  	"vppConfig.ABF":                    names{protoName: "abfs", jsonName: "abfs"},
    49  	"vppConfig.ACL":                    names{protoName: "acls", jsonName: "acls"},
    50  	"vppConfig.SecurityPolicyDatabase": names{protoName: "ipsec_spds", jsonName: "ipsecSpds"},
    51  	"vppConfig.SecurityPolicy":         names{protoName: "ipsec_sps", jsonName: "ipsecSps"},
    52  	"vppConfig.SecurityAssociation":    names{protoName: "ipsec_sas", jsonName: "ipsecSas"},
    53  	"vppConfig.TunnelProtection":       names{protoName: "ipsec_tunnel_protections", jsonName: "ipsecTunnelProtections"},
    54  	"vppConfig.Interface":              names{protoName: "interfaces", jsonName: "interfaces"},
    55  	"vppConfig.Span":                   names{protoName: "spans", jsonName: "spans"},
    56  	"vppConfig.IPFIX":                  names{protoName: "ipfix_global", jsonName: "ipfixGlobal"},
    57  	"vppConfig.FlowProbeParams":        names{protoName: "ipfix_flowprobe_params", jsonName: "ipfixFlowprobeParams"},
    58  	"vppConfig.FlowProbeFeature":       names{protoName: "ipfix_flowprobes", jsonName: "ipfixFlowprobes"},
    59  	"vppConfig.BridgeDomain":           names{protoName: "bridge_domains", jsonName: "bridgeDomains"},
    60  	"vppConfig.FIBEntry":               names{protoName: "fibs", jsonName: "fibs"},
    61  	"vppConfig.XConnectPair":           names{protoName: "xconnect_pairs", jsonName: "xconnectPairs"},
    62  	"vppConfig.ARPEntry":               names{protoName: "arps", jsonName: "arps"},
    63  	"vppConfig.Route":                  names{protoName: "routes", jsonName: "routes"},
    64  	"vppConfig.ProxyARP":               names{protoName: "proxy_arp", jsonName: "proxyArp"},
    65  	"vppConfig.IPScanNeighbor":         names{protoName: "ipscan_neighbor", jsonName: "ipscanNeighbor"},
    66  	"vppConfig.VrfTable":               names{protoName: "vrfs", jsonName: "vrfs"},
    67  	"vppConfig.DHCPProxy":              names{protoName: "dhcp_proxies", jsonName: "dhcpProxies"},
    68  	"vppConfig.L3XConnect":             names{protoName: "l3xconnects", jsonName: "l3xconnects"},
    69  	"vppConfig.TeibEntry":              names{protoName: "teib_entries", jsonName: "teibEntries"},
    70  	"vppConfig.Nat44Global":            names{protoName: "nat44_global", jsonName: "nat44Global"},
    71  	"vppConfig.DNat44":                 names{protoName: "dnat44s", jsonName: "dnat44s"},
    72  	"vppConfig.Nat44Interface":         names{protoName: "nat44_interfaces", jsonName: "nat44Interfaces"},
    73  	"vppConfig.Nat44AddressPool":       names{protoName: "nat44_pools", jsonName: "nat44Pools"},
    74  	"vppConfig.IPRedirect":             names{protoName: "punt_ipredirects", jsonName: "puntIpredirects"},
    75  	"vppConfig.ToHost":                 names{protoName: "punt_tohosts", jsonName: "puntTohosts"},
    76  	"vppConfig.Exception":              names{protoName: "punt_exceptions", jsonName: "puntExceptions"},
    77  	"vppConfig.LocalSID":               names{protoName: "srv6_localsids", jsonName: "srv6Localsids"},
    78  	"vppConfig.Policy":                 names{protoName: "srv6_policies", jsonName: "srv6Policies"},
    79  	"vppConfig.Steering":               names{protoName: "srv6_steerings", jsonName: "srv6Steerings"},
    80  	"vppConfig.SRv6Global":             names{protoName: "srv6_global", jsonName: "srv6Global"},
    81  }
    82  
    83  // NewDynamicConfig creates dynamically proto Message that contains all given configuration models(knowModels).
    84  // This proto message(when all VPP-Agent models are given as input) is json/yaml compatible with
    85  // configurator.Config. The configurator.Config config have all models hardcoded (generated from config
    86  // proto model, but that model is hardcoded). Dynamic config can contain also custom 3rd party models
    87  // and therefore can be used to import/export config data also for 3rd party models that are registered, but not
    88  // part of VPP-Agent repository and therefore not know to hardcoded configurator.Config.
    89  func NewDynamicConfig(knownModels []*models.ModelInfo) (*dynamicpb.Message, error) {
    90  	// create dependency registry
    91  	dependencyRegistry, err := createFileDescRegistry(knownModels)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("cannot create dependency file descriptor registry due to: %w", err)
    94  	}
    95  
    96  	// get file descriptor proto for give known models
    97  	fileDP, rootMsgName, err := createDynamicConfigDescriptorProto(knownModels, dependencyRegistry)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("cannot create descriptor proto for dynamic config due to: %v", err)
   100  	}
   101  
   102  	// convert file descriptor proto to file descriptor
   103  	fd, err := protodesc.NewFile(fileDP, dependencyRegistry)
   104  	if err != nil {
   105  		return nil, fmt.Errorf("cannot convert file descriptor proto to file descriptor due to: %v", err)
   106  	}
   107  
   108  	// get descriptor for config root message
   109  	rootMsg := fd.Messages().ByName(rootMsgName)
   110  
   111  	// create dynamic config message
   112  	return dynamicpb.NewMessage(rootMsg), nil
   113  }
   114  
   115  // createFileDescRegistry extracts file descriptors from given known models and returns them in convenient
   116  // registry (in form of protodesc.Resolver).
   117  func createFileDescRegistry(knownModels []*models.ModelInfo) (protodesc.Resolver, error) {
   118  	reg := &protoregistry.Files{}
   119  	for _, knownModel := range knownModels {
   120  		fileDesc := knownModel.MessageDescriptor.ParentFile()
   121  		if _, err := reg.FindDescriptorByName(fileDesc.FullName()); err == protoregistry.NotFound {
   122  			if e := reg.RegisterFile(fileDesc); e != nil {
   123  				logrus.DefaultLogger().Warnf("Failed to add Proto file %v "+
   124  					"to dependency registry: %v.", fileDesc.Path(), e)
   125  			} else {
   126  				logrus.DefaultLogger().Debugf("Proto file %v was successfully "+
   127  					"added to dependency registry.", fileDesc.Path())
   128  			}
   129  		}
   130  	}
   131  	return reg, nil
   132  }
   133  
   134  // createDynamicConfigDescriptorProto creates descriptor proto for configuration. The construction of the descriptor
   135  // proto is the way how the configuration from known models are added to the configuration proto message.
   136  // The constructed file descriptor proto is used to get file descriptor that in turn can be used to instantiate
   137  // proto message with all the configs from knownModels. This method conveniently provides also the configuration
   138  // root message (proto file has many messages, but we need to know which one is the root for our configuration).
   139  func createDynamicConfigDescriptorProto(knownModels []*ModelInfo, dependencyRegistry protodesc.Resolver) (
   140  	fileDP *descriptorpb.FileDescriptorProto, rootMsgName protoreflect.Name, error error) {
   141  
   142  	// file descriptor proto for dynamic config proto model
   143  	fileDP = &descriptorpb.FileDescriptorProto{
   144  		Syntax:  proto.String("proto3"),
   145  		Name:    proto.String("ligato/configurator/dynamicconfigurator.proto"),
   146  		Package: proto.String("ligato.configurator"),
   147  	}
   148  
   149  	// create config message
   150  	configDP := &descriptorpb.DescriptorProto{
   151  		Name: proto.String("Dynamic_config"),
   152  	}
   153  
   154  	// add config message to proto file
   155  	fileDP.MessageType = []*descriptorpb.DescriptorProto{configDP}
   156  
   157  	// define configuration root (for users of this function)
   158  	rootMsgName = protoreflect.Name(*(configDP.Name))
   159  
   160  	// fill dynamic message with given known models
   161  	configGroups := make(map[string]*descriptorpb.DescriptorProto)
   162  	importedDependency := make(map[string]struct{}) // just for deduplication checking
   163  	for _, modelDetail := range knownModels {
   164  		// get/create group config for this know model (all configs are grouped into groups based on their module)
   165  		configGroupName := DynamicConfigGroupFieldNaming(modelDetail)
   166  		configGroup, found := configGroups[configGroupName]
   167  		if !found { // create it!
   168  			// create new message that represents new config group
   169  			configGroup = &descriptorpb.DescriptorProto{
   170  				Name: proto.String(configGroupName),
   171  			}
   172  
   173  			// add config group message to message definitions
   174  			fileDP.MessageType = append(fileDP.MessageType, configGroup)
   175  
   176  			// create reference to the new config group message from main config message
   177  			configDP.Field = append(configDP.Field, &descriptorpb.FieldDescriptorProto{
   178  				Name:     proto.String(configGroupName),
   179  				Number:   proto.Int32(int32(len(configDP.Field) + 1)),
   180  				JsonName: proto.String(configGroupName),
   181  				Type:     protoType(descriptorpb.FieldDescriptorProto_TYPE_MESSAGE),
   182  				TypeName: proto.String(fmt.Sprintf(".%v.%v", *fileDP.Package, *configGroup.Name)),
   183  				Label:    protoLabel(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL),
   184  			})
   185  
   186  			// cache config group for reuse by other known models
   187  			configGroups[configGroupName] = configGroup
   188  		}
   189  
   190  		// fill config group message with currently handled known model
   191  		label := protoLabel(descriptorpb.FieldDescriptorProto_LABEL_REPEATED)
   192  		if !existsModelOptionFor("nameTemplate", modelDetail.Options) {
   193  			label = protoLabel(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL)
   194  		}
   195  		protoName, jsonName := DynamicConfigKnownModelFieldNaming(modelDetail)
   196  		configGroup.Field = append(configGroup.Field, &descriptorpb.FieldDescriptorProto{
   197  			Name:     proto.String(protoName),
   198  			Number:   proto.Int32(int32(len(configGroup.Field) + 1)),
   199  			JsonName: proto.String(jsonName),
   200  			Label:    label,
   201  			Type:     protoType(descriptorpb.FieldDescriptorProto_TYPE_MESSAGE),
   202  			TypeName: proto.String(fmt.Sprintf(".%v", modelDetail.ProtoName)),
   203  		})
   204  
   205  		// add proto file dependency for this known model (+ check that it is in dependency file descriptor registry)
   206  		protoFile, err := ModelOptionFor("protoFile", modelDetail.Options)
   207  		if err != nil {
   208  			error = fmt.Errorf("cannot retrieve protoFile from model options "+
   209  				"from model %v due to: %v", modelDetail.ProtoName, err)
   210  			return
   211  		}
   212  		if _, found := importedDependency[protoFile]; !found {
   213  			importedDependency[protoFile] = struct{}{}
   214  
   215  			// add proto file dependency for this known model
   216  			fileDP.Dependency = append(fileDP.Dependency, protoFile)
   217  
   218  			// checking dependency registry that should already contain the linked dependency
   219  			if _, err := dependencyRegistry.FindFileByPath(protoFile); err != nil {
   220  				if err == protoregistry.NotFound {
   221  					error = fmt.Errorf("proto file %v need to be referenced in dynamic config, but it "+
   222  						"is not in dependency registry that was created from file descriptor proto input "+
   223  						"(missing in input? check debug output from creating dependency registry) ", protoFile)
   224  					return
   225  				}
   226  				error = fmt.Errorf("cannot verify that proto file %v is in "+
   227  					"dependency registry, it is due to: %v", protoFile, err)
   228  				return
   229  			}
   230  		}
   231  	}
   232  	return
   233  }
   234  
   235  // DynamicConfigGroupFieldNaming computes for given known model the naming of configuration group proto field
   236  // containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration.
   237  // The json name of the field is the same as proto name of field.
   238  func DynamicConfigGroupFieldNaming(modelDetail *models.ModelInfo) string {
   239  	return fmt.Sprintf("%v%v", modulePrefix(models.ToSpec(modelDetail.Spec).ModelName()), configGroupSuffix)
   240  }
   241  
   242  // DynamicConfigKnownModelFieldNaming compute for given known model the (proto and json) naming of proto field
   243  // containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration.
   244  func DynamicConfigKnownModelFieldNaming(modelDetail *models.ModelInfo) (protoName, jsonName string) {
   245  	simpleProtoName := simpleProtoName(modelDetail.ProtoName)
   246  	configGroupName := DynamicConfigGroupFieldNaming(modelDetail)
   247  	compatibilityKey := fmt.Sprintf("%v.%v", configGroupName, simpleProtoName)
   248  
   249  	if newNames, found := backwardCompatibleNames[compatibilityKey]; found {
   250  		// using field names from hardcoded configurator.Config to achieve json/yaml backward compatibility
   251  		protoName = newNames.protoName
   252  		jsonName = newNames.jsonName
   253  	} else if !existsModelOptionFor("nameTemplate", modelDetail.Options) {
   254  		protoName = simpleProtoName
   255  		jsonName = simpleProtoName
   256  	} else {
   257  		protoName = simpleProtoName + repeatedFieldsSuffix
   258  		jsonName = simpleProtoName + repeatedFieldsSuffix
   259  	}
   260  
   261  	return protoName, jsonName
   262  }
   263  
   264  // DynamicConfigExport exports from dynamic config the proto.Messages corresponding to known models that
   265  // were given as input when dynamic config was created using NewDynamicConfig. This is a convenient
   266  // method how to extract data for generic client usage (proto.Message instances) from value-filled
   267  // dynamic config (i.e. after json/yaml loading into dynamic config).
   268  func DynamicConfigExport(dynamicConfig *dynamicpb.Message) ([]proto.Message, error) {
   269  	if dynamicConfig == nil {
   270  		return nil, fmt.Errorf("dynamic config cannot be nil")
   271  	}
   272  
   273  	// iterate over config group messages and extract proto message from them
   274  	result := make([]proto.Message, 0)
   275  	fields := dynamicConfig.Descriptor().Fields()
   276  	for i := 0; i < fields.Len(); i++ {
   277  		fieldName := fields.Get(i).Name()
   278  		if strings.HasSuffix(string(fieldName), configGroupSuffix) {
   279  			configGroupMessage := dynamicConfig.Get(fields.Get(i)).Message()
   280  
   281  			// handling export from inner config layers by using helper method
   282  			result = append(result, exportFromConfigGroupMessage(configGroupMessage)...)
   283  		}
   284  	}
   285  	return result, nil
   286  }
   287  
   288  // ExportDynamicConfigStructure is a debugging helper function revealing current structure of dynamic config.
   289  // Debugging tools can't reveal that because dynamic config is dynamic proto message with no fields named by
   290  // proto fields as it is in generated proto messages.
   291  func ExportDynamicConfigStructure(dynamicConfig proto.Message) (string, error) {
   292  	// fill dynamic message with nothing (one proto message that will not map to anything), but relaying
   293  	// on side effect that will fill the structure with empty messages
   294  	anyProtoMessage := []proto.Message{&generic.Item{}}
   295  	util.PlaceProtosIntoProtos(anyProtoMessage, 1000, dynamicConfig)
   296  
   297  	// export dynamic config to json and then into yaml format
   298  	m := protojson.MarshalOptions{
   299  		Indent: "",
   300  		// this will also fill non-Message fields (Message fields are filled by util.PlaceProtosIntoProtos side effect)
   301  		EmitUnpopulated: true,
   302  	}
   303  	b, err := m.Marshal(dynamicConfig)
   304  	if err != nil {
   305  		return "", fmt.Errorf("cannot marshal dynamic config to json due to: %v", err)
   306  	}
   307  	var jsonObj interface{}
   308  	err = yaml.UnmarshalWithOptions(b, &jsonObj, yaml.UseOrderedMap())
   309  	if err != nil {
   310  		return "", fmt.Errorf("cannot convert dynamic config's json bytes to "+
   311  			"json struct for yaml marshalling due to: %v", err)
   312  	}
   313  	bb, err := yaml.Marshal(jsonObj)
   314  	if err != nil {
   315  		return "", fmt.Errorf("cannot marshal dynamic config from json to yaml due to: %v", err)
   316  	}
   317  	return string(bb), nil
   318  }
   319  
   320  // exportFromConfigGroupMessage exports proto messages from config group message layer of dynamic config
   321  func exportFromConfigGroupMessage(configGroupMessage protoreflect.Message) []proto.Message {
   322  	result := make([]proto.Message, 0)
   323  	fields := configGroupMessage.Descriptor().Fields()
   324  	for i := 0; i < fields.Len(); i++ {
   325  		groupField := fields.Get(i)
   326  		if groupField.IsList() { // repeated field
   327  			repeatedValue := configGroupMessage.Get(groupField).List()
   328  			for j := 0; j < repeatedValue.Len(); j++ {
   329  				result = append(result, repeatedValue.Get(j).Message().Interface())
   330  			}
   331  		} else { // optional field (there are only optional and repeated fields)
   332  			fieldValue := configGroupMessage.Get(groupField)
   333  			if fieldValue.Message().IsValid() {
   334  				// use only non-nil real value (validity check used for this is implementation
   335  				// dependent, but there seems to be no other way)
   336  				result = append(result, fieldValue.Message().Interface())
   337  			}
   338  		}
   339  	}
   340  	return result
   341  }
   342  
   343  func simpleProtoName(fullProtoName string) string {
   344  	nameSplit := strings.Split(fullProtoName, ".")
   345  	return nameSplit[len(nameSplit)-1]
   346  }
   347  
   348  // ModelOptionFor extracts value for given model detail option key
   349  func ModelOptionFor(key string, options []*generic.ModelDetail_Option) (string, error) {
   350  	for _, option := range options {
   351  		if option.Key == key {
   352  			if len(option.Values) == 0 {
   353  				return "", fmt.Errorf("there is no value for key %v in model options", key)
   354  			}
   355  			if strings.TrimSpace(option.Values[0]) == "" {
   356  				return "", fmt.Errorf("there is no value(only empty string "+
   357  					"after trimming) for key %v in model options", key)
   358  			}
   359  			return option.Values[0], nil
   360  		}
   361  	}
   362  	return "", fmt.Errorf("there is no model option with key %v (model options=%+v))", key, options)
   363  }
   364  
   365  func existsModelOptionFor(key string, options []*generic.ModelDetail_Option) bool {
   366  	_, err := ModelOptionFor(key, options)
   367  	return err == nil
   368  }
   369  
   370  func modulePrefix(modelName string) string {
   371  	return strings.Split(modelName, ".")[0] // modelname = modulname(it has modulname prefix) + simple name of model
   372  }
   373  
   374  func protoType(typ descriptorpb.FieldDescriptorProto_Type) *descriptorpb.FieldDescriptorProto_Type {
   375  	return &typ
   376  }
   377  
   378  func protoLabel(label descriptorpb.FieldDescriptorProto_Label) *descriptorpb.FieldDescriptorProto_Label {
   379  	return &label
   380  }