github.com/cilium/cilium@v1.16.2/pkg/bgpv1/agent/annotations.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package agent
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"net/netip"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/cilium/cilium/pkg/annotation"
    14  )
    15  
    16  // ErrNotVRouterAnno is an error returned from parseAnnotation() when the
    17  // the casted string is not a `cilium.io/bgp-virtual-router` annotation
    18  type ErrNotVRouterAnno struct {
    19  	a string
    20  }
    21  
    22  func (e ErrNotVRouterAnno) Error() string {
    23  	return "annotation " + e.a + " is not a valid cilium.io/bgp-virtual-router annotation"
    24  }
    25  
    26  // ErrNoASNAnno is an error returned from parseAnnotation() when the bgp-virtual-router
    27  // annotation does not include a local ASN.
    28  type ErrNoASNAnno struct {
    29  	a string
    30  }
    31  
    32  func (e ErrNoASNAnno) Error() string {
    33  	return "annotation " + e.a + " provides no asn"
    34  }
    35  
    36  // ErrASN is an error returned from parseAnnotation() when the bgp-virtual-router
    37  // annotation includes an ASN that cannot be parsed into an
    38  type ErrASNAnno struct {
    39  	err  string
    40  	asn  string
    41  	anno string
    42  }
    43  
    44  func (e ErrASNAnno) Error() string {
    45  	return "ASN" + e.asn + " in annotation " + e.anno + " cannot be parsed into integer: " + e.err
    46  }
    47  
    48  // ErrAttrib is an error returned from parseAnnotation() when an attribute is
    49  // provided but its value is malformed.
    50  type ErrAttrib struct {
    51  	anno string
    52  	attr string
    53  	err  string
    54  }
    55  
    56  func (e ErrAttrib) Error() string {
    57  	return "annotation " + e.anno + " failed to parse attribute " + e.attr + ":" + e.err
    58  }
    59  
    60  // The BGP control plane may need some node-specific configuration for
    61  // instantiating virtual routers.
    62  //
    63  // For example, BGP router IDs cannot repeat in a BGP peering topology.
    64  // When Cilium cannot generate a unique router ID it will look for a unique
    65  // router ID for the virtual router identified by its local ASN.
    66  //
    67  // We define a set of attributes which can be defined via Node-specific
    68  // kubernetes annotations.
    69  //
    70  // This Kubernetes annotation's syntax is:
    71  // `cilium.io/bgp-virtual-router.{asn}="attr1=value1,attr2=value2"
    72  //
    73  // Where {asn} is replaced by the local ASN of the virtual router.
    74  //
    75  // Currently supported attributes are:
    76  //
    77  //	router-id=IPv4 (string): when present on a specific node, use this value for
    78  //	                         the router ID of the virtual router with local {asn}
    79  //	local-port=port (int):  the local port to listen on for incoming BGP connections
    80  type Attributes struct {
    81  	// The local ASN of the virtual router these Attributes targets.
    82  	ASN int64
    83  	// The router ID to use for the virtual router with the above local ASN.
    84  	RouterID string
    85  	// The local BGP port to listen on.
    86  	LocalPort int32
    87  }
    88  
    89  // AnnotationMap coorelates a parsed Annotations structure with the local
    90  // ASN its annotating.
    91  type AnnotationMap map[int64]Attributes
    92  
    93  func (a AnnotationMap) ResolveRouterID(localASN int64) (string, error) {
    94  	if attr, ok := a[localASN]; ok && attr.RouterID != "" {
    95  		return attr.RouterID, nil
    96  	}
    97  	return "", fmt.Errorf("router id not specified by annotation, cannot resolve router id for local ASN %v", localASN)
    98  }
    99  
   100  // NewAnnotationMap parses a Node's annotations into a AnnotationMap
   101  // and returns the latter.
   102  //
   103  // An error is returned containing one or more parsing errors.
   104  //
   105  // This is for convenience so the caller can log all parsing errors at once.
   106  // The error should still be treated as a normal descrete error and an empty
   107  // AnnotationMap is returned.
   108  func NewAnnotationMap(a map[string]string) (AnnotationMap, error) {
   109  	am := AnnotationMap{}
   110  	errs := make([]error, 0, len(a))
   111  	for key, value := range a {
   112  		asn, attrs, err := parseAnnotation(key, value)
   113  		if err != nil && !errors.As(err, &ErrNotVRouterAnno{}) {
   114  			errs = append(errs, err)
   115  			continue
   116  		}
   117  		am[asn] = attrs
   118  	}
   119  	if len(errs) > 0 {
   120  		return am, errors.Join(errs...)
   121  	}
   122  	return am, nil
   123  }
   124  
   125  // parseAnnotation will attempt to parse a `cilium.io/bgp-virtual-router`
   126  // annotation into an Attributes structure.
   127  //
   128  // Errors returned by this parse method are defined at top of file.
   129  func parseAnnotation(key string, value string) (int64, Attributes, error) {
   130  	var out Attributes
   131  	// is this an annotation we care about?
   132  	if !strings.HasPrefix(key, annotation.BGPVRouterAnnoPrefix) {
   133  		return 0, out, ErrNotVRouterAnno{key}
   134  	}
   135  
   136  	// parse out asn from annotation key, if split at "." will be 3rd element
   137  	var asn int64
   138  	if anno := strings.Split(key, "."); len(anno) != 3 {
   139  		return 0, out, ErrNoASNAnno{key}
   140  	} else {
   141  		asn64, err := strconv.ParseUint(anno[2], 10, 32)
   142  		if err != nil {
   143  			return 0, out, ErrASNAnno{"could not parse ASN as a 32bit integer", anno[2], key}
   144  		}
   145  		asn = int64(asn64)
   146  	}
   147  	out.ASN = asn
   148  
   149  	// split annotation value into multiple "key=value" formatted attributes.
   150  	attrs := strings.Split(value, ",")
   151  	if len(attrs) == 0 {
   152  		return 0, out, nil // empty attributes, not an error
   153  	}
   154  	// parse string attributes into Attributes structure.
   155  	for _, attr := range attrs {
   156  		kv := strings.Split(attr, "=")
   157  		if len(kv) != 2 {
   158  			continue
   159  		}
   160  		switch kv[0] {
   161  		case "router-id":
   162  			addr, err := netip.ParseAddr(kv[1])
   163  			if err != nil {
   164  				return 0, out, ErrAttrib{key, kv[0], "could not parse router-id as an IPv4 address"}
   165  			}
   166  			if !addr.Is4() {
   167  				return 0, out, ErrAttrib{key, kv[0], "router-id must be a valid IPv4 address"}
   168  			}
   169  			out.RouterID = kv[1]
   170  		case "local-port":
   171  			port, err := strconv.ParseInt(kv[1], 10, 16)
   172  			if err != nil {
   173  				return 0, out, ErrAttrib{key, kv[0], "could not parse into port number as 16bit integer"}
   174  			}
   175  			out.LocalPort = int32(port)
   176  		}
   177  	}
   178  	return asn, out, nil
   179  }