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 }