github.com/webmeshproj/webmesh-cni@v0.0.27/internal/cmd/plugin/plugin.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 plugin
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"log/slog"
    23  	"os"
    24  	"path/filepath"
    25  	"runtime/debug"
    26  	"time"
    27  
    28  	"github.com/containernetworking/cni/pkg/skel"
    29  	cnitypes "github.com/containernetworking/cni/pkg/types"
    30  	cniv1 "github.com/containernetworking/cni/pkg/types/100"
    31  	cniversion "github.com/containernetworking/cni/pkg/version"
    32  	"github.com/containernetworking/plugins/pkg/ns"
    33  	"github.com/containernetworking/plugins/pkg/utils/sysctl"
    34  	"github.com/vishvananda/netlink"
    35  	"github.com/webmeshproj/webmesh/pkg/version"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  
    38  	"github.com/webmeshproj/webmesh-cni/internal/types"
    39  )
    40  
    41  //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers,verbs=get;list;watch;create;update;patch;delete
    42  //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers/status,verbs=get;update;patch
    43  //+kubebuilder:rbac:groups=cni.webmesh.io,resources=peercontainers/finalizers,verbs=update
    44  
    45  // TODO: Make these configurable.
    46  const (
    47  	// How long we wait to try to ping the API server before giving up.
    48  	testConnectionTimeout = time.Second * 2
    49  	// How long we wait to try to create the peer container instance.
    50  	createPeerContainerTimeout = time.Second * 3
    51  	// How long to wait for the controller to setup the container interface.
    52  	setupContainerInterfaceTimeout = time.Second * 15
    53  )
    54  
    55  // Main is the entrypoint for the webmesh-cni plugin.
    56  func Main(version version.BuildInfo) {
    57  	// Defer a panic recover, so that in case we panic we can still return
    58  	// a proper error to the runtime.
    59  	defer func() {
    60  		if e := recover(); e != nil {
    61  			msg := fmt.Sprintf("Webmesh CNI panicked: %s\nStack trace:\n%s", e, string(debug.Stack()))
    62  			cnierr := cnitypes.Error{
    63  				Code:    100,
    64  				Msg:     "Webmesh CNI panicked",
    65  				Details: msg,
    66  			}
    67  			cnierr.Print()
    68  			os.Exit(1)
    69  		}
    70  	}()
    71  	skel.PluginMain(
    72  		cmdAdd,
    73  		cmdCheck,
    74  		cmdDel,
    75  		cniversion.PluginSupports("0.3.1"),
    76  		"Webmesh CNI plugin "+version.Version,
    77  	)
    78  }
    79  
    80  // cmdAdd is the CNI ADD command handler.
    81  func cmdAdd(args *skel.CmdArgs) (err error) {
    82  	log := slog.Default()
    83  	result := &cniv1.Result{}
    84  	defer func() {
    85  		if err != nil {
    86  			log.Error("Final result of CNI ADD was an error", "error", err.Error())
    87  			cnierr := cnitypes.Error{
    88  				Code:    100,
    89  				Msg:     "error setting up interface",
    90  				Details: err.Error(),
    91  			}
    92  			cnierr.Print()
    93  			os.Exit(1)
    94  		}
    95  		log.Info("Returning CNI result", "result", result)
    96  		err = cnitypes.PrintResult(result, result.CNIVersion)
    97  		if err != nil {
    98  			log.Error("Failed to print CNI result", "error", err.Error())
    99  			cnierr := cnitypes.Error{
   100  				Code:    100,
   101  				Msg:     "failed to print CNI result",
   102  				Details: err.Error(),
   103  			}
   104  			cnierr.Print()
   105  			os.Exit(1)
   106  		}
   107  	}()
   108  	conf, err := types.LoadNetConfFromArgs(args)
   109  	if err != nil {
   110  		err = fmt.Errorf("failed to load config: %w", err)
   111  		return
   112  	}
   113  	log = conf.NewLogger(args)
   114  	result.CNIVersion = conf.CNIVersion
   115  	result.DNS = conf.DNS
   116  	log.Debug("Handling new container add request")
   117  	cli, err := conf.NewClient(testConnectionTimeout)
   118  	if err != nil {
   119  		err = fmt.Errorf("failed to create client: %w", err)
   120  		return
   121  	}
   122  	// Check if we've already created a PeerContainer for this container.
   123  	log.Debug("Ensuring PeerContainer exists")
   124  	ctx, cancel := context.WithTimeout(context.Background(), createPeerContainerTimeout)
   125  	defer cancel()
   126  	err = cli.EnsureContainer(ctx, args)
   127  	if err != nil {
   128  		log.Error("Failed to ensure PeerContainer", "error", err.Error())
   129  		err = fmt.Errorf("failed to ensure PeerContainer: %w", err)
   130  		return
   131  	}
   132  	// Wait for the PeerContainer to be ready.
   133  	log.Debug("Waiting for PeerContainer to be ready")
   134  	ctx, cancel = context.WithTimeout(context.Background(), setupContainerInterfaceTimeout)
   135  	defer cancel()
   136  	peerContainer, err := cli.WaitForRunning(ctx, args)
   137  	if err != nil {
   138  		log.Error("Failed to wait for PeerContainer to be ready", "error", err.Error())
   139  		err = fmt.Errorf("failed to wait for PeerContainer to be ready: %w", err)
   140  		return
   141  	}
   142  	ifname := peerContainer.Status.InterfaceName
   143  	// Parse the IP addresses from the container status.
   144  	log.Debug("Building container interface result from status", "status", peerContainer.Status)
   145  	err = peerContainer.AppendToResults(result)
   146  	if err != nil {
   147  		log.Error("Failed to build container interface result from status", "error", err.Error())
   148  		err = fmt.Errorf("failed to build container interface result from status: %w", err)
   149  		return
   150  	}
   151  	if len(peerContainer.Status.DNSServers) > 0 {
   152  		// We need to create a special resolv conf for the network namespace.
   153  		log.Debug("Creating resolv.conf for container namespace")
   154  		resolvConfPath := filepath.Join("/etc/netns", filepath.Base(args.Netns), "resolv.conf")
   155  		err := os.MkdirAll(filepath.Dir(resolvConfPath), 0755)
   156  		if err != nil {
   157  			err = fmt.Errorf("failed to create resolv.conf directory: %w", err)
   158  			return err
   159  		}
   160  		resolvConf, err := os.Create(resolvConfPath)
   161  		if err != nil {
   162  			err = fmt.Errorf("failed to create resolv.conf: %w", err)
   163  			return err
   164  		}
   165  		defer resolvConf.Close()
   166  		for _, dnsServer := range peerContainer.Status.DNSServers {
   167  			_, err = resolvConf.WriteString(fmt.Sprintf("nameserver %s\n", dnsServer))
   168  			if err != nil {
   169  				err = fmt.Errorf("failed to write to resolv.conf: %w", err)
   170  				return err
   171  			}
   172  		}
   173  	}
   174  	// Get the interface details from the container namespace and ensure IP forwarding is enabled.
   175  	log.Debug("Getting interface details from container namespace")
   176  	containerNs, err := ns.GetNS(args.Netns)
   177  	if err != nil {
   178  		err = fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
   179  		return
   180  	}
   181  	defer containerNs.Close()
   182  	err = containerNs.Do(func(_ ns.NetNS) (err error) {
   183  		link, err := netlink.LinkByName(ifname)
   184  		if err != nil {
   185  			err = fmt.Errorf("failed to find %q: %v", ifname, err)
   186  			return
   187  		}
   188  		result.Interfaces = []*cniv1.Interface{{
   189  			Name:    link.Attrs().Name,
   190  			Mac:     link.Attrs().HardwareAddr.String(),
   191  			Sandbox: containerNs.Path(),
   192  		}}
   193  		if !conf.Interface.DisableIPv6 {
   194  			log.Debug("Enabling IPv6 forwarding")
   195  			_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/forwarding", link.Attrs().Name), "1")
   196  		}
   197  		if !conf.Interface.DisableIPv4 {
   198  			log.Debug("Enabling IPv4 forwarding")
   199  			_, _ = sysctl.Sysctl(fmt.Sprintf("net/ipv4/conf/%s/forwarding", link.Attrs().Name), "1")
   200  		}
   201  		return
   202  	})
   203  	return
   204  }
   205  
   206  // cmdCheck is the CNI CHECK command handler. Most implementations do a dummy check like this.
   207  // TODO: This could be used to check if there are new routes to track.
   208  func cmdCheck(args *skel.CmdArgs) (err error) {
   209  	log := slog.Default()
   210  	defer func() {
   211  		if err != nil {
   212  			log.Error("Final result of CNI CHECK was an error", "error", err.Error())
   213  			cnierr := cnitypes.Error{
   214  				Code:    100,
   215  				Msg:     "error checking interface",
   216  				Details: err.Error(),
   217  			}
   218  			cnierr.Print()
   219  			os.Exit(1)
   220  		}
   221  	}()
   222  	conf, err := types.LoadNetConfFromArgs(args)
   223  	if err != nil {
   224  		err = fmt.Errorf("failed to load config: %w", err)
   225  		return
   226  	}
   227  	log = conf.NewLogger(args)
   228  	log.Debug("Handling new CHECK request")
   229  	_, err = conf.NewClient(testConnectionTimeout)
   230  	if err != nil {
   231  		err = fmt.Errorf("failed to create client: %w", err)
   232  		return
   233  	}
   234  	fmt.Println("OK")
   235  	return
   236  }
   237  
   238  // cmdDel is the CNI DEL command handler.
   239  func cmdDel(args *skel.CmdArgs) (err error) {
   240  	log := slog.Default()
   241  	defer func() {
   242  		if err != nil {
   243  			log.Error("Final result of CNI DEL was an error", "error", err.Error())
   244  			cnierr := cnitypes.Error{
   245  				Code:    100,
   246  				Msg:     "error deleting interface",
   247  				Details: err.Error(),
   248  			}
   249  			cnierr.Print()
   250  			os.Exit(1)
   251  		}
   252  	}()
   253  	conf, err := types.LoadNetConfFromArgs(args)
   254  	if err != nil {
   255  		err = fmt.Errorf("failed to load config: %w", err)
   256  		return
   257  	}
   258  	log = conf.NewLogger(args)
   259  	// Delete the PeerContainer.
   260  	log.Debug("Deleting PeerContainer", "container", conf.ObjectKeyFromArgs(args))
   261  	cli, err := conf.NewClient(testConnectionTimeout)
   262  	if err != nil {
   263  		err = fmt.Errorf("failed to create client: %w", err)
   264  		return
   265  	}
   266  	ctx, cancel := context.WithTimeout(context.Background(), testConnectionTimeout)
   267  	defer cancel()
   268  	err = cli.DeletePeerContainer(ctx, args)
   269  	if err != nil && client.IgnoreNotFound(err) != nil {
   270  		log.Error("Failed to delete PeerContainer", "error", err.Error())
   271  		err = fmt.Errorf("failed to delete PeerContainer: %w", err)
   272  		return
   273  	}
   274  	fmt.Println("OK")
   275  	return
   276  }