2023-10-07 08:52:31 -05:00

404 lines
13 KiB
Go

package utils
import (
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strings"
)
const (
noIPsSpecifiedErrorMsg = "no IP ranges specified"
)
type cniNetworkConfig struct {
FilePath string
Conf *Conf
ConfList *ConfList
}
func NewCNINetworkConfig(cniConfFilePath string) (*cniNetworkConfig, error) {
cniNetConf := cniNetworkConfig{
FilePath: cniConfFilePath,
}
cniFileBytes, err := os.ReadFile(cniConfFilePath)
if err != nil {
return nil, fmt.Errorf("error reading %s: %v", cniConfFilePath, err)
}
// If we're working with a conflist setup
if cniNetConf.IsConfList() {
confList := new(ConfList)
err = json.Unmarshal(cniFileBytes, confList)
if err != nil {
return nil, fmt.Errorf("failed to load CNI conflist file: %v", err)
}
if len(confList.Plugins) == 0 {
return nil, fmt.Errorf("CNI config list %s has no plugins", cniConfFilePath)
}
cniNetConf.ConfList = confList
} else {
// If we're working with a conf setup
conf := new(Conf)
err = json.Unmarshal(cniFileBytes, conf)
if err != nil {
return nil, fmt.Errorf("failed to load CNI conf file: %v", err)
}
if conf.Type == "" {
return nil, fmt.Errorf("error load CNI config, file appears to have no type: %s", cniConfFilePath)
}
cniNetConf.Conf = conf
}
if err = cniNetConf.consolidateSubnets(); err != nil {
return nil, err
}
return &cniNetConf, nil
}
// consolidateSubnets Many people still define the legacy single subnet variation of the IPAM plugin instead of the
// newer ranges variation. To account for this and make parsing simpler, we do the same thing that the official IPAM
// config loader does and collapse them into ranges.
func (c *cniNetworkConfig) consolidateSubnets() error {
brPlug := c.getBridgePlugin()
if brPlug.IPAM.Subnet != "" {
err := c.InsertPodCIDRIntoIPAM(brPlug.IPAM.Subnet)
if err != nil {
return err
}
brPlug.IPAM.Subnet = ""
delete(brPlug.IPAM.raw, "subnet")
}
return nil
}
// IsConfList checks to see if this CNI configuration is a *.conflist file or if it is a *.conf file. Returns true for
// *.conflist, returns false for anything else.
func (c *cniNetworkConfig) IsConfList() bool {
return strings.HasSuffix(strings.ToLower(c.FilePath), ".conflist")
}
// getPodCIDRsMapFromCNISpec gets pod CIDR allocated to the node as a map from CNI spec file and returns it
func (c *cniNetworkConfig) getPodCIDRsMapFromCNISpec() (map[string]*net.IPNet, error) {
podCIDRs := make(map[string]*net.IPNet)
var err error
ipamConfig := c.getBridgePlugin().IPAM
if err != nil {
if err.Error() != noIPsSpecifiedErrorMsg {
return nil, err
}
return nil, nil
}
// Parse ranges from ipamConfig
if ipamConfig != nil && len(ipamConfig.Ranges) > 0 {
for _, rangeSet := range ipamConfig.Ranges {
for _, item := range rangeSet {
if item.Subnet != "" {
_, netCIDR, err := net.ParseCIDR(item.Subnet)
if err != nil {
return nil, fmt.Errorf("unable to parse CIDR '%s' contained in CNI: %s",
item.Subnet, c.FilePath)
}
podCIDRs[netCIDR.String()] = netCIDR
}
}
}
}
return podCIDRs, nil
}
// GetPodCIDRsFromCNISpec gets pod CIDR allocated to the node from CNI spec file and returns it
func (c *cniNetworkConfig) GetPodCIDRsFromCNISpec() ([]*net.IPNet, error) {
podCIDRMap, err := c.getPodCIDRsMapFromCNISpec()
if err != nil {
return nil, err
}
podCIDRs := make([]*net.IPNet, 0)
for _, podCIDR := range podCIDRMap {
podCIDRs = append(podCIDRs, podCIDR)
}
return podCIDRs, nil
}
// getBridgePlugin get the bridge plugin configuration out of the cniNetworkConfig in a consistent manner
func (c *cniNetworkConfig) getBridgePlugin() *Conf {
if c.ConfList != nil {
for _, conf := range c.ConfList.Plugins {
if conf.Type == "bridge" {
return conf
}
}
}
return c.Conf
}
// InsertPodCIDRIntoIPAM insert a new cidr into the CNI file. If the CIDR already exists in the CNI ranges, then
// operation is a noop. Throws an error if either the passed cidr cannot be parsed or if there is a problem with the
// CIDRs already in the CNI config.
func (c *cniNetworkConfig) InsertPodCIDRIntoIPAM(cidr string) error {
ipamConfig := c.getBridgePlugin().IPAM
// This should have already been sanitized by the GetPodCIDR* functions before it comes to us, but you can never be
// too safe...
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("unable to parse input cidr: %s - %v", cidr, err)
}
// Check that we don't already have the cidr in our list of ranges already, if so, consider it a no-op
existingPodCIDRs, err := c.getPodCIDRsMapFromCNISpec()
if err != nil {
return err
}
if _, ok := existingPodCIDRs[cidr]; ok {
return nil
}
// Add the CIDR that was passed to us
newRange := []*Range{{raw: make(map[string]json.RawMessage), Subnet: cidr}}
ipamConfig.Ranges = append(ipamConfig.Ranges, newRange)
return nil
}
func (c *cniNetworkConfig) SetMTU(mtu int) {
brPlugin := c.getBridgePlugin()
brPlugin.MTU = float64(mtu)
}
func (c *cniNetworkConfig) WriteCNIConfig() error {
var cniBytes []byte
var err error
if c.IsConfList() {
cniBytes, err = json.Marshal(c.ConfList)
if err != nil {
return fmt.Errorf("unable to marshal CNI ConfList: %v", err)
}
} else {
cniBytes, err = json.Marshal(c.Conf)
if err != nil {
return fmt.Errorf("unable to marshal CNI Conf: %v", err)
}
}
err = os.WriteFile(c.FilePath, cniBytes, 0644)
if err != nil {
return fmt.Errorf("failed to write into CNI conf file: %v", err)
}
return nil
}
// This elaborate re-definition of the stuff inside libcni is necessary because the upstream cni structs and funcs were
// only ever meant to unmarshal data. Since each struct only defines the needs of the specific plugins (like bridge or
// ipam), they often times leave out the fields belonging to other plugins. This means when we go to marshal the data
// back into JSON we'll drop fields that are important to other plugins.
//
// So instead, we create a special set of utility structs and funcs that are capable of partially unmarshal-ing JSON
// data. Parsing the information we care about, and leaving the data that we don't care about and may not even know
// about alone. Then it is able to faithfully re-marshal the resulting structs back into their JSON form without losing
// data.
//
// This is very similar to the way that the PerimeterX/marshmallow (https://github.com/PerimeterX/marshmallow) library
// works, except that these functions are capable of marshaling the JSON back to its original form reliably. Whereas
// marshmallow is only able to unmarshal the data[
// rawMapAble interface that denotes an object for which we are able to convert it to a list of keys associated with
// raw JSON byte data
type rawMapAble interface {
getRaw() *map[string]json.RawMessage
}
// All of our CNI based structs are listed here. Each struct only has the fields that we use to specifically read or
// write data to / from.
// ConfList represents a list of CNI configurations
type ConfList struct {
Plugins []*Conf
raw map[string]json.RawMessage
}
// Conf represents the individual CNI configuration that may exist on its own, or be part of a ConfList
type Conf struct {
Bridge string
IPAM *IPAM
MTU float64
Type string
raw map[string]json.RawMessage
}
// IPAM represents the ipam specific configuration that may exist on a given CNI configuration / plugin
type IPAM struct {
Subnet string
Ranges [][]*Range
raw map[string]json.RawMessage
}
// Range represents an IP range that may exist within a range set (hence the double array above)
type Range struct {
Subnet string
raw map[string]json.RawMessage
}
// The following are the implementations of rawMapAble, json.Marshaler, & json.Unmarshaler for each of the above
// structs. Each struct requires the following methods in order to be marshaled / unmarshaled:
// * getRaw() *map[string]json.RawMessage
// * UnmarshalJSON(bytes []byte) error
// * MarshalJson() ([]bytes, error)
func (c *ConfList) getRaw() *map[string]json.RawMessage {
return &c.raw
}
func (c *ConfList) UnmarshalJSON(bytes []byte) error {
return PartialJSONUnmarshal(c, bytes)
}
func (c *ConfList) MarshalJSON() ([]byte, error) {
return PartialJSONMarshal(c)
}
func (c *Conf) getRaw() *map[string]json.RawMessage {
return &c.raw
}
func (c *Conf) UnmarshalJSON(bytes []byte) error {
return PartialJSONUnmarshal(c, bytes)
}
func (c *Conf) MarshalJSON() ([]byte, error) {
return PartialJSONMarshal(c)
}
func (i *IPAM) getRaw() *map[string]json.RawMessage {
return &i.raw
}
func (i *IPAM) UnmarshalJSON(bytes []byte) error {
return PartialJSONUnmarshal(i, bytes)
}
func (i *IPAM) MarshalJSON() ([]byte, error) {
return PartialJSONMarshal(i)
}
func (r *Range) getRaw() *map[string]json.RawMessage {
return &r.raw
}
func (r *Range) UnmarshalJSON(bytes []byte) error {
return PartialJSONUnmarshal(r, bytes)
}
func (r *Range) MarshalJSON() ([]byte, error) {
return PartialJSONMarshal(r)
}
// PartialJSONUnmarshal allows a struct that implements the rawMapAble interface to be partially unmarshaled. This means
// that via this function we are able to parse and understand the fields that we know about and have defined in the
// struct without knowing every possible field. This still stores the unknown fields and they can be retrieved via the
// getRaw() function and restored properly via the PartialJSONMarshal() function.
func PartialJSONUnmarshal(r rawMapAble, bytes []byte) error {
// Unmarshal the full element into map[string]json.RawMessage so that we can ensure that we capture all elements
// and not just the ones that we have struct fields for
raw := r.getRaw()
if err := json.Unmarshal(bytes, raw); err != nil {
return err
}
// Go through the struct that lies under the rawMapAble interface and loop through all of its fields
val := reflect.ValueOf(r).Elem()
for i := 0; i < val.NumField(); i++ {
// Get the name and value of the field for later use
name := strings.ToLower(val.Type().Field(i).Name)
valueField := val.Field(i)
if name == "raw" {
continue
}
// If a name from the underlying struct exists in the raw message, then send it for more complete unmarshalling
if valFromRaw, ok := (*raw)[name]; ok {
if !valueField.CanAddr() {
// Make sure that the value is capable of addressing before we try it below and get a panic
continue
}
// Unmarshal the raw JSON into the specific interface of the underlying struct, this second pass at
// unmarshalling is how we populate specific fields in our struct rather than just working with raw JSON
if err := json.Unmarshal(valFromRaw, valueField.Addr().Interface()); err != nil {
return err
}
}
}
return nil
}
// isNilOrEmpty Unfortunately, we cannot blindly call the IsNil() function on reflect.Value as there are many types like
// strings that are not able to have a nil value and it will cause a panic
func isNilOrEmpty(v reflect.Value) bool {
//nolint:exhaustive // we don't care about all of the potential types here, only the ones that might trip us up
switch v.Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return v.IsNil()
case reflect.String:
return v.Interface() == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8,
reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
return v.IsZero()
}
return false
}
// PartialJSONMarshal allows a struct that implements the rawMapAble interface to be fully restored without having
// to know about every possible field that may exist within the JSON. This is the reverse process of
// PartialJSONUnmarshal().
func PartialJSONMarshal(r rawMapAble) ([]byte, error) {
raw := r.getRaw()
// Find the value of our RawAble struct passed in
val := reflect.ValueOf(r).Elem()
// Iterate over all the fields in the passed struct
for i := 0; i < val.NumField(); i++ {
name := strings.ToLower(val.Type().Field(i).Name)
valueField := val.Field(i)
if name == "raw" {
// Don't attempt to marshal our raw field as that's where we are marshaling to
continue
}
if !valueField.CanAddr() {
// Make sure that the value is capable of addressing before we try it below and get a panic
continue
}
if isNilOrEmpty(valueField) {
// Don't load up the marshaled JSON with a bunch of null values
continue
}
// We are now reasonably certain that we have a field on the passed struct that is:
// * not our raw field
// * can be addressed
// * is not nil
// Let's marshal it!
bytes, err := json.Marshal(valueField.Addr().Interface())
if err != nil {
return nil, err
}
// Take the marshaled value and store it in the raw map alongside other keys that we don't care about and were
// never unmarshalled
(*raw)[name] = bytes
}
// Finally marshal our raw map which contains both parsed and unparsed fields
return json.Marshal(raw)
}