github.com/webmeshproj/webmesh-cni@v0.0.27/internal/types/netconf.go (about)

     1  /*
     2  Copyright 2023 Avi Zimmerman <avi.zimmerman@gmail.com>.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package types
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"log/slog"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/containernetworking/cni/pkg/skel"
    30  	cnitypes "github.com/containernetworking/cni/pkg/types"
    31  	meshsys "github.com/webmeshproj/webmesh/pkg/meshnet/system"
    32  	meshtypes "github.com/webmeshproj/webmesh/pkg/storage/types"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/client-go/rest"
    35  	"k8s.io/client-go/tools/clientcmd"
    36  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  
    39  	meshcniv1 "github.com/webmeshproj/webmesh-cni/api/v1"
    40  )
    41  
    42  // NetConf is the configuration for the CNI plugin.
    43  type NetConf struct {
    44  	// NetConf is the typed configuration for the CNI plugin.
    45  	cnitypes.NetConf `json:",inline"`
    46  
    47  	// Interface is the configuration for container interfaces.
    48  	Interface Interface `json:"interface,omitempty"`
    49  	// Kubernetes is the configuration for the Kubernetes API server and
    50  	// information about the node we are running on.
    51  	Kubernetes Kubernetes `json:"kubernetes,omitempty"`
    52  	// LogLevel is the log level for the plugin and managed interfaces.
    53  	LogLevel string `json:"logLevel,omitempty"`
    54  	// LogFile is the file to write logs to.
    55  	LogFile string `json:"logFile,omitempty"`
    56  }
    57  
    58  // SetDefaults sets the default values for the configuration.
    59  // It returns the configuration for convenience.
    60  func (n *NetConf) SetDefaults() *NetConf {
    61  	if n == nil {
    62  		n = &NetConf{}
    63  	}
    64  	n.Kubernetes.Default()
    65  	n.Interface.Default()
    66  	if n.LogLevel == "" {
    67  		n.LogLevel = "info"
    68  	}
    69  	return n
    70  }
    71  
    72  // DeepEqual returns whether the configuration is equal to the given configuration.
    73  func (n *NetConf) DeepEqual(other *NetConf) bool {
    74  	if n == nil && other == nil {
    75  		return true
    76  	}
    77  	if n == nil || other == nil {
    78  		return false
    79  	}
    80  	return n.Kubernetes.DeepEqual(&other.Kubernetes) &&
    81  		n.Interface.DeepEqual(&other.Interface) &&
    82  		n.LogLevel == other.LogLevel
    83  }
    84  
    85  // Interface is the configuration for a single interface.
    86  type Interface struct {
    87  	// MTU is the MTU to set on interfaces.
    88  	MTU int `json:"mtu,omitempty"`
    89  	// DisableIPv4 is whether to disable IPv4 on the interface.
    90  	DisableIPv4 bool `json:"disableIPv4,omitempty"`
    91  	// DisableIPv6 is whether to disable IPv6 on the interface.
    92  	DisableIPv6 bool `json:"disableIPv6,omitempty"`
    93  }
    94  
    95  // Default sets the default values for the interface configuration.
    96  func (i *Interface) Default() {
    97  	if i.MTU <= 0 {
    98  		i.MTU = meshsys.DefaultMTU
    99  	}
   100  }
   101  
   102  // DeepEqual returns whether the interface is equal to the given interface.
   103  func (i *Interface) DeepEqual(other *Interface) bool {
   104  	if i == nil && other == nil {
   105  		return true
   106  	}
   107  	if i == nil || other == nil {
   108  		return false
   109  	}
   110  	return i.MTU == other.MTU &&
   111  		i.DisableIPv4 == other.DisableIPv4 &&
   112  		i.DisableIPv6 == other.DisableIPv6
   113  }
   114  
   115  // Kubernetes is the configuration for the Kubernetes API server and
   116  // information about the node we are running on.
   117  type Kubernetes struct {
   118  	// Kubeconfig is the path to the kubeconfig file.
   119  	Kubeconfig string `json:"kubeconfig,omitempty"`
   120  	// NodeName is the name of the node we are running on.
   121  	NodeName string `json:"nodeName,omitempty"`
   122  	// K8sAPIRoot is the root URL of the Kubernetes API server.
   123  	K8sAPIRoot string `json:"k8sAPIRoot,omitempty"`
   124  	// Namespace is the namespace to use for the plugin.
   125  	Namespace string `json:"namespace,omitempty"`
   126  }
   127  
   128  // Default sets the default values for the Kubernetes configuration.
   129  func (k *Kubernetes) Default() {
   130  	if k.Kubeconfig == "" {
   131  		k.Kubeconfig = DefaultKubeconfigPath
   132  	}
   133  	if k.Namespace == "" {
   134  		k.Namespace = DefaultNamespace
   135  	}
   136  }
   137  
   138  // DeepEqual returns whether the Kubernetes configuration is equal to the given configuration.
   139  func (k *Kubernetes) DeepEqual(other *Kubernetes) bool {
   140  	if k == nil && other == nil {
   141  		return true
   142  	}
   143  	if k == nil || other == nil {
   144  		return false
   145  	}
   146  	return k.Kubeconfig == other.Kubeconfig &&
   147  		k.NodeName == other.NodeName &&
   148  		k.K8sAPIRoot == other.K8sAPIRoot &&
   149  		k.Namespace == other.Namespace
   150  }
   151  
   152  // LoadDefaultNetConf attempts to load the configuration from the default file.
   153  func LoadDefaultNetConf() (*NetConf, error) {
   154  	return LoadNetConfFromFile(DefaultNetConfPath)
   155  }
   156  
   157  // LoadNetConfFromFile loads the configuration from the given file.
   158  func LoadNetConfFromFile(path string) (*NetConf, error) {
   159  	data, err := os.ReadFile(path)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("failed to read config file: %w", err)
   162  	}
   163  	return DecodeNetConf(data)
   164  }
   165  
   166  // LoadConfigFromArgs loads the configuration from the given CNI arguments.
   167  func LoadNetConfFromArgs(cmd *skel.CmdArgs) (*NetConf, error) {
   168  	return DecodeNetConf(cmd.StdinData)
   169  }
   170  
   171  // DecodeNetConf loads the configuration from the given JSON data.
   172  func DecodeNetConf(data []byte) (*NetConf, error) {
   173  	var conf NetConf
   174  	err := json.Unmarshal(data, &conf)
   175  	if err != nil {
   176  		return nil, fmt.Errorf("failed to load netconf from data: %w", err)
   177  	}
   178  	return conf.SetDefaults(), nil
   179  }
   180  
   181  // NewLogger creates a new logger for the plugin.
   182  func (n *NetConf) NewLogger(args *skel.CmdArgs) *slog.Logger {
   183  	return slog.New(slog.NewJSONHandler(n.LogWriter(), &slog.HandlerOptions{
   184  		AddSource: true,
   185  		Level:     n.SlogLevel(),
   186  	})).
   187  		With("container", n.ObjectKeyFromArgs(args)).
   188  		With("args", args).
   189  		With("config", n)
   190  }
   191  
   192  // SlogLevel returns the slog.Level for the given log level string.
   193  func (n *NetConf) SlogLevel() slog.Level {
   194  	switch strings.ToLower(n.LogLevel) {
   195  	case "debug":
   196  		return slog.LevelDebug
   197  	case "info":
   198  		return slog.LevelInfo
   199  	case "warn":
   200  		return slog.LevelWarn
   201  	case "error":
   202  		return slog.LevelError
   203  	default:
   204  		return slog.LevelInfo
   205  	}
   206  }
   207  
   208  // LogWriter reteurns the io.Writer for the plugin logger.
   209  func (n *NetConf) LogWriter() io.Writer {
   210  	switch strings.ToLower(n.LogLevel) {
   211  	case "silent", "off":
   212  		return io.Discard
   213  	}
   214  	if n.LogFile != "" {
   215  		err := os.MkdirAll(filepath.Dir(n.LogFile), 0755)
   216  		if err != nil {
   217  			fmt.Fprintf(os.Stderr, "failed to create log directory, falling back to stderr: %v", err)
   218  			return os.Stderr
   219  		}
   220  		f, err := os.OpenFile(n.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   221  		if err != nil {
   222  			fmt.Fprintf(os.Stderr, "failed to open log file, falling back to stderr: %v", err)
   223  			return os.Stderr
   224  		}
   225  		return f
   226  	}
   227  	return os.Stderr
   228  }
   229  
   230  // ObjectKeyFromArgs creates a new object key for the given container ID.
   231  func (n *NetConf) ObjectKeyFromArgs(args *skel.CmdArgs) client.ObjectKey {
   232  	return client.ObjectKey{
   233  		Name:      meshtypes.TruncateID(args.ContainerID),
   234  		Namespace: n.Kubernetes.Namespace,
   235  	}
   236  }
   237  
   238  // ContainerFromArgs creates a skeleton container object for the given container arguments.
   239  func (n *NetConf) ContainerFromArgs(args *skel.CmdArgs) meshcniv1.PeerContainer {
   240  	return meshcniv1.PeerContainer{
   241  		TypeMeta: metav1.TypeMeta{
   242  			Kind:       "PeerContainer",
   243  			APIVersion: meshcniv1.GroupVersion.String(),
   244  		},
   245  		ObjectMeta: metav1.ObjectMeta{
   246  			Name:      meshtypes.TruncateID(args.ContainerID),
   247  			Namespace: n.Kubernetes.Namespace,
   248  		},
   249  		Spec: meshcniv1.PeerContainerSpec{
   250  			NodeID:      meshtypes.TruncateID(args.ContainerID),
   251  			ContainerID: args.ContainerID,
   252  			Netns:       args.Netns,
   253  			IfName:      IfNameFromID(meshtypes.TruncateID(args.ContainerID)),
   254  			NodeName:    n.Kubernetes.NodeName,
   255  			MTU:         n.Interface.MTU,
   256  			DisableIPv4: n.Interface.DisableIPv4,
   257  			DisableIPv6: n.Interface.DisableIPv6,
   258  			LogLevel:    n.LogLevel,
   259  		},
   260  	}
   261  }
   262  
   263  // IfNameFromID returns a suitable interface name for the given identifier.
   264  func IfNameFromID(id string) string {
   265  	id = strings.Replace(id, "-", "", -1)
   266  	id = strings.Replace(id, "_", "", -1)
   267  	return IfacePrefix + id[:min(9, len(id))] + "0"
   268  }
   269  
   270  // NewClient creates a new client for the Kubernetes API server.
   271  func (n *NetConf) NewClient(pingTimeout time.Duration) (*Client, error) {
   272  	if n == nil {
   273  		return nil, fmt.Errorf("netconf is nil")
   274  	}
   275  	restCfg, err := n.RestConfig()
   276  	if err != nil {
   277  		err = fmt.Errorf("failed to create REST config: %w", err)
   278  		return nil, err
   279  	}
   280  	cli, err := NewClientForConfig(ClientConfig{
   281  		RestConfig: restCfg,
   282  		NetConf:    n,
   283  	})
   284  	if err != nil {
   285  		err = fmt.Errorf("failed to create client: %w", err)
   286  		return nil, err
   287  	}
   288  	return cli, cli.Ping(pingTimeout)
   289  }
   290  
   291  // RestConfig returns the rest config for the Kubernetes API server.
   292  func (n *NetConf) RestConfig() (*rest.Config, error) {
   293  	cfg, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) {
   294  		conf, err := clientcmd.LoadFromFile(n.Kubernetes.Kubeconfig)
   295  		if err != nil {
   296  			return nil, fmt.Errorf("failed to load kubeconfig from file: %w", err)
   297  		}
   298  		return conf, nil
   299  	})
   300  	return cfg, err
   301  }