From c24708ff62bb54f209b1e8b10d2f826ceed0060b Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Wed, 15 Apr 2015 15:35:02 -0700 Subject: [PATCH 001/134] Add plugin code This adds basic plugins. "main" types: veth, bridge, macvlan "ipam" type: host-local The code has been ported over from github.com/coreos/rkt project and adapted to fit the CNI spec. --- ip/cidr.go | 86 ++++++++++++++++++++++++++++++ ip/ipmasq.go | 66 +++++++++++++++++++++++ ip/link.go | 117 +++++++++++++++++++++++++++++++++++++++++ ip/route.go | 47 +++++++++++++++++ ns/ns.go | 81 ++++++++++++++++++++++++++++ plugin/ipam.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ plugin/types.go | 106 +++++++++++++++++++++++++++++++++++++ skel/skel.go | 98 ++++++++++++++++++++++++++++++++++ 8 files changed, 737 insertions(+) create mode 100644 ip/cidr.go create mode 100644 ip/ipmasq.go create mode 100644 ip/link.go create mode 100644 ip/route.go create mode 100644 ns/ns.go create mode 100644 plugin/ipam.go create mode 100644 plugin/types.go create mode 100644 skel/skel.go diff --git a/ip/cidr.go b/ip/cidr.go new file mode 100644 index 00000000..c9633988 --- /dev/null +++ b/ip/cidr.go @@ -0,0 +1,86 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "encoding/json" + "math/big" + "net" +) + +// ParseCIDR takes a string like "10.2.3.1/24" and +// return IPNet with "10.2.3.1" and /24 mask +func ParseCIDR(s string) (*net.IPNet, error) { + ip, ipn, err := net.ParseCIDR(s) + if err != nil { + return nil, err + } + + ipn.IP = ip + return ipn, nil +} + +// NextIP returns IP incremented by 1 +func NextIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Add(i, big.NewInt(1))) +} + +// PrevIP returns IP decremented by 1 +func PrevIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Sub(i, big.NewInt(1))) +} + +func ipToInt(ip net.IP) *big.Int { + if v := ip.To4(); v != nil { + return big.NewInt(0).SetBytes(v) + } + return big.NewInt(0).SetBytes(ip.To16()) +} + +func intToIP(i *big.Int) net.IP { + return net.IP(i.Bytes()) +} + +// Network masks off the host portion of the IP +func Network(ipn *net.IPNet) *net.IPNet { + return &net.IPNet{ + IP: ipn.IP.Mask(ipn.Mask), + Mask: ipn.Mask, + } +} + +// like net.IPNet but adds JSON marshalling and unmarshalling +type IPNet net.IPNet + +func (n IPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(&n).String()) +} + +func (n *IPNet) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + tmp, err := ParseCIDR(s) + if err != nil { + return err + } + + *n = IPNet(*tmp) + return nil +} diff --git a/ip/ipmasq.go b/ip/ipmasq.go new file mode 100644 index 00000000..665189bc --- /dev/null +++ b/ip/ipmasq.go @@ -0,0 +1,66 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "fmt" + "net" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables" +) + +// SetupIPMasq installs iptables rules to masquerade traffic +// coming from ipn and going outside of it +func SetupIPMasq(ipn *net.IPNet, chain string) error { + ipt, err := iptables.New() + if err != nil { + return fmt.Errorf("failed to locate iptabes: %v", err) + } + + if err = ipt.NewChain("nat", chain); err != nil { + if err.(*iptables.Error).ExitStatus() != 1 { + // TODO(eyakubovich): assumes exit status 1 implies chain exists + return err + } + } + + if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT"); err != nil { + return err + } + + if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE"); err != nil { + return err + } + + return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain) +} + +// TeardownIPMasq undoes the effects of SetupIPMasq +func TeardownIPMasq(ipn *net.IPNet, chain string) error { + ipt, err := iptables.New() + if err != nil { + return fmt.Errorf("failed to locate iptabes: %v", err) + } + + if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain); err != nil { + return err + } + + if err = ipt.ClearChain("nat", chain); err != nil { + return err + } + + return ipt.DeleteChain("nat", chain) +} diff --git a/ip/link.go b/ip/link.go new file mode 100644 index 00000000..59865cf8 --- /dev/null +++ b/ip/link.go @@ -0,0 +1,117 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "crypto/sha512" + "fmt" + "net" + "os" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" +) + +func makeVeth(name, peer string, mtu int) (netlink.Link, error) { + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + Flags: net.FlagUp, + MTU: mtu, + }, + PeerName: peer, + } + if err := netlink.LinkAdd(veth); err != nil { + return nil, err + } + + return veth, nil +} + +// RandomVethName returns string "veth" with random prefix (hashed from entropy) +func RandomVethName(entropy string) string { + h := sha512.New() + h.Write([]byte(entropy)) + return fmt.Sprintf("veth%x", h.Sum(nil)[:5]) +} + +// SetupVeth sets up a virtual ethernet link. +// Should be in container netns. +// TODO(eyakubovich): get rid of entropy and ask kernel to pick name via pattern +func SetupVeth(entropy, contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { + // NetworkManager (recent versions) will ignore veth devices that start with "veth" + hostVethName := RandomVethName(entropy) + hostVeth, err = makeVeth(hostVethName, contVethName, mtu) + if err != nil { + err = fmt.Errorf("failed to make veth pair: %v", err) + return + } + + if err = netlink.LinkSetUp(hostVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", hostVethName, err) + return + } + + contVeth, err = netlink.LinkByName(contVethName) + if err != nil { + err = fmt.Errorf("failed to lookup %q: %v", contVethName, err) + return + } + + if err = netlink.LinkSetUp(contVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", contVethName, err) + return + } + + if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { + err = fmt.Errorf("failed to move veth to host netns: %v", err) + return + } + + return +} + +// DelLinkByName removes an interface link. +func DelLinkByName(ifName string) error { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + return nil +} + +// DelLinkByNameAddr remove an interface returns its IP address +// of the specified family +func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + addrs, err := netlink.AddrList(iface, family) + if err != nil || len(addrs) == 0 { + return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return nil, fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + return addrs[0].IPNet, nil +} diff --git a/ip/route.go b/ip/route.go new file mode 100644 index 00000000..f310f1e3 --- /dev/null +++ b/ip/route.go @@ -0,0 +1,47 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "net" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" +) + +// AddDefaultRoute sets the default route on the given gateway. +func AddDefaultRoute(gw net.IP, dev netlink.Link) error { + _, defNet, _ := net.ParseCIDR("0.0.0.0/0") + return AddRoute(defNet, gw, dev) +} + +// AddRoute adds a universally-scoped route to a device. +func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_UNIVERSE, + Dst: ipn, + Gw: gw, + }) +} + +// AddHostRoute adds a host-scoped route to a device. +func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_HOST, + Dst: ipn, + Gw: gw, + }) +} diff --git a/ns/ns.go b/ns/ns.go new file mode 100644 index 00000000..82291f98 --- /dev/null +++ b/ns/ns.go @@ -0,0 +1,81 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +import ( + "fmt" + "os" + "runtime" + "syscall" +) + +var setNsMap = map[string]uintptr{ + "386": 346, + "amd64": 308, + "arm": 374, +} + +// SetNS sets the network namespace on a target file. +func SetNS(f *os.File, flags uintptr) error { + if runtime.GOOS != "linux" { + return fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } + + trap, ok := setNsMap[runtime.GOARCH] + if !ok { + return fmt.Errorf("unsupported arch: %s", runtime.GOARCH) + } + + _, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0) + if err != 0 { + return err + } + + return nil +} + +// WithNetNSPath executes the passed closure under the given network +// namespace, restoring the original namespace afterwards. +func WithNetNSPath(nspath string, f func(*os.File) error) error { + ns, err := os.Open(nspath) + if err != nil { + return fmt.Errorf("Failed to open %v: %v", nspath, err) + } + defer ns.Close() + + return WithNetNS(ns, f) +} + +// WithNetNS executes the passed closure under the given network +// namespace, restoring the original namespace afterwards. +func WithNetNS(ns *os.File, f func(*os.File) error) error { + // save a handle to current (host) network namespace + thisNS, err := os.Open("/proc/self/ns/net") + if err != nil { + return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err) + } + defer thisNS.Close() + + if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil { + return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err) + } + + if err = f(thisNS); err != nil { + return err + } + + // switch back + return SetNS(thisNS, syscall.CLONE_NEWNET) +} diff --git a/plugin/ipam.go b/plugin/ipam.go new file mode 100644 index 00000000..8b59cab7 --- /dev/null +++ b/plugin/ipam.go @@ -0,0 +1,136 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" + "github.com/appc/cni/pkg/ip" +) + +// Find returns the full path of the plugin by searching in CNI_PATH +func Find(plugin string) string { + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + for _, p := range paths { + fullname := filepath.Join(p, plugin) + if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { + return fullname + } + } + + return "" +} + +// ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD. +// Parses and returns resulting IPConfig +func ExecAdd(plugin string, netconf []byte) (*Result, error) { + if os.Getenv("CNI_COMMAND") != "ADD" { + return nil, fmt.Errorf("CNI_COMMAND is not ADD") + } + + pluginPath := Find(plugin) + if pluginPath == "" { + return nil, fmt.Errorf("could not find %q plugin", plugin) + } + + stdout := &bytes.Buffer{} + + c := exec.Cmd{ + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stdout: stdout, + Stderr: os.Stderr, + } + if err := c.Run(); err != nil { + return nil, err + } + + res := &Result{} + err := json.Unmarshal(stdout.Bytes(), res) + return res, err +} + +// ExecDel executes IPAM plugin, assuming CNI_COMMAND == DEL. +func ExecDel(plugin string, netconf []byte) error { + if os.Getenv("CNI_COMMAND") != "DEL" { + return fmt.Errorf("CNI_COMMAND is not DEL") + } + + pluginPath := Find(plugin) + if pluginPath == "" { + return fmt.Errorf("could not find %q plugin", plugin) + } + + c := exec.Cmd{ + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stderr: os.Stderr, + } + return c.Run() +} + +// ConfigureIface takes the result of IPAM plugin and +// applies to the ifName interface +func ConfigureIface(ifName string, res *Result) error { + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed too set %q UP: %v", ifName, err) + } + + // TODO(eyakubovich): IPv6 + addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) + } + + for _, r := range res.IP4.Routes { + gw := r.GW + if gw == nil { + gw = res.IP4.Gateway + } + if err = ip.AddRoute(&r.Dst, gw, link); err != nil { + // we skip over duplicate routes as we assume the first one wins + if !os.IsExist(err) { + return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) + } + } + } + + return nil +} + +// PrintResult writes out prettified Result to stdout +func PrintResult(res *Result) error { + data, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(data) + return err +} diff --git a/plugin/types.go b/plugin/types.go new file mode 100644 index 00000000..6eb6ac21 --- /dev/null +++ b/plugin/types.go @@ -0,0 +1,106 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "encoding/json" + "net" + + "github.com/appc/cni/pkg/ip" +) + +// NetConf describes a network. +type NetConf struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + IPAM struct { + Type string `json:"type,omitempty"` + } `json:"ipam,omitempty"` +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` +} + +// IPConfig contains values necessary to configure an interface +type IPConfig struct { + IP net.IPNet + Gateway net.IP + Routes []Route +} + +type Route struct { + Dst net.IPNet + GW net.IP +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom ip.IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP ip.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []Route `json:"routes,omitempty"` +} + +type route struct { + Dst ip.IPNet `json:"dst"` + GW net.IP `json:"gw,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: ip.IPNet(c.IP), + Gateway: c.Gateway, + Routes: c.Routes, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.IP = net.IPNet(ipc.IP) + c.Gateway = ipc.Gateway + c.Routes = ipc.Routes + return nil +} + +func (r *Route) UnmarshalJSON(data []byte) error { + rt := route{} + if err := json.Unmarshal(data, &rt); err != nil { + return err + } + + r.Dst = net.IPNet(rt.Dst) + r.GW = rt.GW + return nil +} + +func (r *Route) MarshalJSON() ([]byte, error) { + rt := route{ + Dst: ip.IPNet(r.Dst), + GW: r.GW, + } + + return json.Marshal(rt) +} diff --git a/skel/skel.go b/skel/skel.go new file mode 100644 index 00000000..9f033350 --- /dev/null +++ b/skel/skel.go @@ -0,0 +1,98 @@ +// Copyright 2014 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package skel provides skeleton code for a CNI plugin. +// In particular, it implements argument parsing and validation. +package skel + +import ( + "io/ioutil" + "log" + "os" +) + +// CmdArgs captures all the arguments passed in to the plugin +// via both env vars and stdin +type CmdArgs struct { + ContainerID string + Netns string + IfName string + Args string + Path string + StdinData []byte +} + +// PluginMain is the "main" for a plugin. It accepts +// two callback functions for add and del commands. +func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { + var cmd, contID, netns, ifName, args, path string + + vars := []struct { + name string + val *string + req bool + }{ + {"CNI_COMMAND", &cmd, true}, + {"CNI_CONTAINERID", &contID, false}, + {"CNI_NETNS", &netns, true}, + {"CNI_IFNAME", &ifName, true}, + {"CNI_ARGS", &args, false}, + {"CNI_PATH", &path, true}, + } + + argsMissing := false + for _, v := range vars { + *v.val = os.Getenv(v.name) + if v.req && *v.val == "" { + log.Printf("%v env variable missing", v.name) + argsMissing = true + } + } + + if argsMissing { + os.Exit(1) + } + + stdinData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Printf("Error reading from stdin: %v", err) + os.Exit(1) + } + + cmdArgs := &CmdArgs{ + ContainerID: contID, + Netns: netns, + IfName: ifName, + Args: args, + Path: path, + StdinData: stdinData, + } + + switch cmd { + case "ADD": + err = cmdAdd(cmdArgs) + + case "DEL": + err = cmdDel(cmdArgs) + + default: + log.Printf("Unknown CNI_COMMAND: %v", cmd) + os.Exit(1) + } + + if err != nil { + log.Printf("%v: %v", cmd, err) + os.Exit(1) + } +} From 93be8e63f1ced70e81fe3d978f325cdbc7be5eda Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Tue, 5 May 2015 13:35:20 -0700 Subject: [PATCH 002/134] Do not use netns as ID or for entropy ContainerID is now required so use that or generate random bytes. Fixes #5 --- ip/link.go | 74 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/ip/link.go b/ip/link.go index 59865cf8..c99f4f3c 100644 --- a/ip/link.go +++ b/ip/link.go @@ -15,7 +15,7 @@ package ip import ( - "crypto/sha512" + "crypto/rand" "fmt" "net" "os" @@ -23,7 +23,7 @@ import ( "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" ) -func makeVeth(name, peer string, mtu int) (netlink.Link, error) { +func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { veth := &netlink.Veth{ LinkAttrs: netlink.LinkAttrs{ Name: name, @@ -39,33 +39,50 @@ func makeVeth(name, peer string, mtu int) (netlink.Link, error) { return veth, nil } +func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) { + for i := 0; i < 10; i++ { + peerName, err = RandomVethName() + if err != nil { + return + } + + veth, err = makeVethPair(name, peerName, mtu) + switch { + case err == nil: + return + + case os.IsExist(err): + continue + + default: + err = fmt.Errorf("failed to make veth pair: %v", err) + return + } + } + + // should really never be hit + err = fmt.Errorf("failed to find a unique veth name") + return +} + // RandomVethName returns string "veth" with random prefix (hashed from entropy) -func RandomVethName(entropy string) string { - h := sha512.New() - h.Write([]byte(entropy)) - return fmt.Sprintf("veth%x", h.Sum(nil)[:5]) +func RandomVethName() (string, error) { + entropy := make([]byte, 4) + _, err := rand.Reader.Read(entropy) + if err != nil { + return "", fmt.Errorf("failed to generate random veth name: %v", err) + } + + // NetworkManager (recent versions) will ignore veth devices that start with "veth" + return fmt.Sprintf("veth%x", entropy), nil } // SetupVeth sets up a virtual ethernet link. // Should be in container netns. -// TODO(eyakubovich): get rid of entropy and ask kernel to pick name via pattern -func SetupVeth(entropy, contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { - // NetworkManager (recent versions) will ignore veth devices that start with "veth" - hostVethName := RandomVethName(entropy) - hostVeth, err = makeVeth(hostVethName, contVethName, mtu) +func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { + var hostVethName string + hostVethName, contVeth, err = makeVeth(contVethName, mtu) if err != nil { - err = fmt.Errorf("failed to make veth pair: %v", err) - return - } - - if err = netlink.LinkSetUp(hostVeth); err != nil { - err = fmt.Errorf("failed to set %q up: %v", hostVethName, err) - return - } - - contVeth, err = netlink.LinkByName(contVethName) - if err != nil { - err = fmt.Errorf("failed to lookup %q: %v", contVethName, err) return } @@ -74,6 +91,17 @@ func SetupVeth(entropy, contVethName string, mtu int, hostNS *os.File) (hostVeth return } + hostVeth, err = netlink.LinkByName(hostVethName) + if err != nil { + err = fmt.Errorf("failed to lookup %q: %v", hostVethName, err) + return + } + + if err = netlink.LinkSetUp(hostVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", contVethName, err) + return + } + if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { err = fmt.Errorf("failed to move veth to host netns: %v", err) return From c7a1442830c796632f317b9b80fc081b4167e25f Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Wed, 29 Apr 2015 17:52:41 -0700 Subject: [PATCH 003/134] report errors via JSON This reflects the latest SPEC draft. --- plugin/ipam.go | 13 +++++++++++-- plugin/types.go | 6 ++++++ skel/skel.go | 22 +++++++++++++++------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/plugin/ipam.go b/plugin/ipam.go index 8b59cab7..4124d0f8 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -125,9 +125,18 @@ func ConfigureIface(ifName string, res *Result) error { return nil } -// PrintResult writes out prettified Result to stdout +// PrintResult writes out prettified Result JSON to stdout func PrintResult(res *Result) error { - data, err := json.MarshalIndent(res, "", " ") + return prettyPrint(res) +} + +// PrintError writes out prettified Error JSON to stdout +func PrintError(err *Error) error { + return prettyPrint(err) +} + +func prettyPrint(obj interface{}) error { + data, err := json.MarshalIndent(obj, "", " ") if err != nil { return err } diff --git a/plugin/types.go b/plugin/types.go index 6eb6ac21..9db0f10e 100644 --- a/plugin/types.go +++ b/plugin/types.go @@ -48,6 +48,12 @@ type Route struct { GW net.IP } +type Error struct { + Code uint `json:"code"` + Msg string `json:"msg"` + Details string `json:"details,omitempty"` +} + // net.IPNet is not JSON (un)marshallable so this duality is needed // for our custom ip.IPNet type diff --git a/skel/skel.go b/skel/skel.go index 9f033350..4f159adb 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -17,9 +17,12 @@ package skel import ( + "fmt" "io/ioutil" "log" "os" + + "github.com/appc/cni/pkg/plugin" ) // CmdArgs captures all the arguments passed in to the plugin @@ -61,13 +64,12 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } if argsMissing { - os.Exit(1) + die("required env variables missing") } stdinData, err := ioutil.ReadAll(os.Stdin) if err != nil { - log.Printf("Error reading from stdin: %v", err) - os.Exit(1) + die("error reading from stdin: %v", err) } cmdArgs := &CmdArgs{ @@ -87,12 +89,18 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { err = cmdDel(cmdArgs) default: - log.Printf("Unknown CNI_COMMAND: %v", cmd) - os.Exit(1) + die("unknown CNI_COMMAND: %v", cmd) } if err != nil { - log.Printf("%v: %v", cmd, err) - os.Exit(1) + die(err.Error()) } } + +func die(f string, args ...interface{}) { + plugin.PrintError(&plugin.Error{ + Code: 100, + Msg: fmt.Sprintf(f, args...), + }) + os.Exit(1) +} From a6a822268c637bc99ff5f7967e3a9d757e504ea0 Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Tue, 19 May 2015 12:02:41 -0700 Subject: [PATCH 004/134] add DHCP IPAM plugin The plugin binary actually functions in two modes. The first mode is a regular CNI plugin. The second mode (when stared with "daemon" arg) runs a DHCP client daemon. When executed as a CNI plugin, it issues an RPC request to the daemon for actual processing. The daemon is required since a DHCP lease needs to be maintained by periodically renewing it. One instance of the daemon can server arbitrary number of containers/leases. --- ns/ns.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/ns/ns.go b/ns/ns.go index 82291f98..20548b9b 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -48,19 +48,30 @@ func SetNS(f *os.File, flags uintptr) error { // WithNetNSPath executes the passed closure under the given network // namespace, restoring the original namespace afterwards. -func WithNetNSPath(nspath string, f func(*os.File) error) error { +// Changing namespaces must be done on a goroutine that has been +// locked to an OS thread. If lockThread arg is true, this function +// locks the goroutine prior to change namespace and unlocks before +// returning +func WithNetNSPath(nspath string, lockThread bool, f func(*os.File) error) error { ns, err := os.Open(nspath) if err != nil { return fmt.Errorf("Failed to open %v: %v", nspath, err) } defer ns.Close() - - return WithNetNS(ns, f) + return WithNetNS(ns, lockThread, f) } // WithNetNS executes the passed closure under the given network // namespace, restoring the original namespace afterwards. -func WithNetNS(ns *os.File, f func(*os.File) error) error { +// Changing namespaces must be done on a goroutine that has been +// locked to an OS thread. If lockThread arg is true, this function +// locks the goroutine prior to change namespace and unlocks before +// returning +func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error { + if lockThread { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + } // save a handle to current (host) network namespace thisNS, err := os.Open("/proc/self/ns/net") if err != nil { From 31c0dbf11095325de431cdc62ba50b5a9e41c218 Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Mon, 1 Jun 2015 17:34:00 -0700 Subject: [PATCH 005/134] Propagate json error object to the caller When plugin errors out, it prints out a JSON object to stdout describing the failure. This object needs to be propagated out through the plugins and to the container runtime. This change also adds Print method to both the result and error structs for easy serialization to stdout. --- plugin/ipam.go | 45 ++++++++++++++++++++++++--------------------- plugin/types.go | 22 ++++++++++++++++++++++ skel/skel.go | 25 ++++++++++++++++++------- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/plugin/ipam.go b/plugin/ipam.go index 4124d0f8..f5a50333 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -41,6 +41,22 @@ func Find(plugin string) string { return "" } +func pluginErr(err error, output []byte) error { + if _, ok := err.(*exec.ExitError); ok { + emsg := Error{} + if perr := json.Unmarshal(output, &emsg); perr != nil { + return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) + } + details := "" + if emsg.Details != "" { + details = fmt.Sprintf("; %v", emsg.Details) + } + return fmt.Errorf("%v%v", emsg.Msg, details) + } + + return err +} + // ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD. // Parses and returns resulting IPConfig func ExecAdd(plugin string, netconf []byte) (*Result, error) { @@ -63,7 +79,7 @@ func ExecAdd(plugin string, netconf []byte) (*Result, error) { Stderr: os.Stderr, } if err := c.Run(); err != nil { - return nil, err + return nil, pluginErr(err, stdout.Bytes()) } res := &Result{} @@ -82,13 +98,19 @@ func ExecDel(plugin string, netconf []byte) error { return fmt.Errorf("could not find %q plugin", plugin) } + stdout := &bytes.Buffer{} + c := exec.Cmd{ Path: pluginPath, Args: []string{pluginPath}, Stdin: bytes.NewBuffer(netconf), + Stdout: stdout, Stderr: os.Stderr, } - return c.Run() + if err := c.Run(); err != nil { + return pluginErr(err, stdout.Bytes()) + } + return nil } // ConfigureIface takes the result of IPAM plugin and @@ -124,22 +146,3 @@ func ConfigureIface(ifName string, res *Result) error { return nil } - -// PrintResult writes out prettified Result JSON to stdout -func PrintResult(res *Result) error { - return prettyPrint(res) -} - -// PrintError writes out prettified Error JSON to stdout -func PrintError(err *Error) error { - return prettyPrint(err) -} - -func prettyPrint(obj interface{}) error { - data, err := json.MarshalIndent(obj, "", " ") - if err != nil { - return err - } - _, err = os.Stdout.Write(data) - return err -} diff --git a/plugin/types.go b/plugin/types.go index 9db0f10e..d5952dde 100644 --- a/plugin/types.go +++ b/plugin/types.go @@ -17,6 +17,7 @@ package plugin import ( "encoding/json" "net" + "os" "github.com/appc/cni/pkg/ip" ) @@ -36,6 +37,10 @@ type Result struct { IP6 *IPConfig `json:"ip6,omitempty"` } +func (r *Result) Print() error { + return prettyPrint(r) +} + // IPConfig contains values necessary to configure an interface type IPConfig struct { IP net.IPNet @@ -54,6 +59,14 @@ type Error struct { Details string `json:"details,omitempty"` } +func (e *Error) Error() string { + return e.Msg +} + +func (e *Error) Print() error { + return prettyPrint(e) +} + // net.IPNet is not JSON (un)marshallable so this duality is needed // for our custom ip.IPNet type @@ -110,3 +123,12 @@ func (r *Route) MarshalJSON() ([]byte, error) { return json.Marshal(rt) } + +func prettyPrint(obj interface{}) error { + data, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(data) + return err +} diff --git a/skel/skel.go b/skel/skel.go index 4f159adb..bf79b91d 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -64,12 +64,12 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } if argsMissing { - die("required env variables missing") + dieMsg("required env variables missing") } stdinData, err := ioutil.ReadAll(os.Stdin) if err != nil { - die("error reading from stdin: %v", err) + dieMsg("error reading from stdin: %v", err) } cmdArgs := &CmdArgs{ @@ -89,18 +89,29 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { err = cmdDel(cmdArgs) default: - die("unknown CNI_COMMAND: %v", cmd) + dieMsg("unknown CNI_COMMAND: %v", cmd) } if err != nil { - die(err.Error()) + if e, ok := err.(*plugin.Error); ok { + // don't wrap Error in Error + dieErr(e) + } + dieMsg(err.Error()) } } -func die(f string, args ...interface{}) { - plugin.PrintError(&plugin.Error{ +func dieMsg(f string, args ...interface{}) { + e := &plugin.Error{ Code: 100, Msg: fmt.Sprintf(f, args...), - }) + } + dieErr(e) +} + +func dieErr(e *plugin.Error) { + if err := e.Print(); err != nil { + log.Print("Error writing error JSON to stdout: ", err) + } os.Exit(1) } From 6caffd48b391a82d66865d4fe8d8ca7e667a936e Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Fri, 12 Jun 2015 12:15:11 -0700 Subject: [PATCH 006/134] No more path rewriting Path rewriting causes too many problems when vendoring vendored code. When CNI code is vendored into rkt, godep has problems code already vendored by CNI. --- ip/ipmasq.go | 2 +- ip/link.go | 2 +- ip/route.go | 2 +- plugin/ipam.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ip/ipmasq.go b/ip/ipmasq.go index 665189bc..7f8740df 100644 --- a/ip/ipmasq.go +++ b/ip/ipmasq.go @@ -18,7 +18,7 @@ import ( "fmt" "net" - "github.com/appc/cni/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables" + "github.com/coreos/go-iptables/iptables" ) // SetupIPMasq installs iptables rules to masquerade traffic diff --git a/ip/link.go b/ip/link.go index c99f4f3c..3936ed9d 100644 --- a/ip/link.go +++ b/ip/link.go @@ -20,7 +20,7 @@ import ( "net" "os" - "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" + "github.com/vishvananda/netlink" ) func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { diff --git a/ip/route.go b/ip/route.go index f310f1e3..04e660ef 100644 --- a/ip/route.go +++ b/ip/route.go @@ -17,7 +17,7 @@ package ip import ( "net" - "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" + "github.com/vishvananda/netlink" ) // AddDefaultRoute sets the default route on the given gateway. diff --git a/plugin/ipam.go b/plugin/ipam.go index f5a50333..6718ed24 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -23,8 +23,8 @@ import ( "path/filepath" "strings" - "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" "github.com/appc/cni/pkg/ip" + "github.com/vishvananda/netlink" ) // Find returns the full path of the plugin by searching in CNI_PATH From 4f74b7804186c75e0e55a7cdb2409769bff7509f Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Mon, 13 Jul 2015 15:41:02 -0700 Subject: [PATCH 007/134] enable net.ipv4.ip_forward in plugins that need it --- ip/ipforward.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ip/ipforward.go diff --git a/ip/ipforward.go b/ip/ipforward.go new file mode 100644 index 00000000..0a1ca252 --- /dev/null +++ b/ip/ipforward.go @@ -0,0 +1,31 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "io/ioutil" +) + +func EnableIP4Forward() error { + return echo1("/proc/sys/net/ipv4/ip_forward") +} + +func EnableIP6Forward() error { + return echo1("/proc/sys/net/ipv6/conf/all/forwarding") +} + +func echo1(f string) error { + return ioutil.WriteFile(f, []byte("1"), 0644) +} From d1a09053456f6a9f075d2f4c4dfc0e79144c0153 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Sun, 16 Aug 2015 02:30:04 +0200 Subject: [PATCH 008/134] host-local: allow ip request via CNI_ARGS A specific IP can now be requested via the environment variable CNI_ARGS, e.g. `CNI_ARGS=ip=1.2.3.4`. The plugin will try to reserve the specified IP. If this is not successful the execution will fail. --- plugin/args.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 plugin/args.go diff --git a/plugin/args.go b/plugin/args.go new file mode 100644 index 00000000..84895793 --- /dev/null +++ b/plugin/args.go @@ -0,0 +1,36 @@ +package plugin + +import ( + "encoding" + "fmt" + "reflect" + "strings" +) + +func LoadArgs(args string, container interface{}) error { + if args == "" { + return nil + } + + containerValue := reflect.ValueOf(container) + + pairs := strings.Split(args, ",") + for _, pair := range pairs { + kv := strings.Split(pair, "=") + if len(kv) != 2 { + return fmt.Errorf("ARGS: invalid pair %q", pair) + } + keyString := kv[0] + valueString := kv[1] + keyField := containerValue.Elem().FieldByName(keyString) + if !keyField.IsValid() { + return fmt.Errorf("ARGS: invalid key %q", keyString) + } + u := keyField.Addr().Interface().(encoding.TextUnmarshaler) + err := u.UnmarshalText([]byte(valueString)) + if err != nil { + return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err) + } + } + return nil +} From 169f8e0a5219e94f828024993b47204c018f5334 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Sat, 5 Sep 2015 18:58:58 +0200 Subject: [PATCH 009/134] CNI_ARGS: use ';' to split args as documented --- plugin/args.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/args.go b/plugin/args.go index 84895793..274ec66b 100644 --- a/plugin/args.go +++ b/plugin/args.go @@ -14,7 +14,7 @@ func LoadArgs(args string, container interface{}) error { containerValue := reflect.ValueOf(container) - pairs := strings.Split(args, ",") + pairs := strings.Split(args, ";") for _, pair := range pairs { kv := strings.Split(pair, "=") if len(kv) != 2 { From ac7cd562fd3852372ffdb28dd7d55902a10be3cc Mon Sep 17 00:00:00 2001 From: Jonathan Boulle Date: Mon, 7 Sep 2015 15:43:34 -0700 Subject: [PATCH 010/134] plugin/ipam: fix typo in error message --- plugin/ipam.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/ipam.go b/plugin/ipam.go index 6718ed24..93a0d5c9 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -122,7 +122,7 @@ func ConfigureIface(ifName string, res *Result) error { } if err := netlink.LinkSetUp(link); err != nil { - return fmt.Errorf("failed too set %q UP: %v", ifName, err) + return fmt.Errorf("failed to set %q UP: %v", ifName, err) } // TODO(eyakubovich): IPv6 From b5db5d3075b5e65bfa9bcb72ffd9a39218f3b9f8 Mon Sep 17 00:00:00 2001 From: Neil Wilson Date: Mon, 7 Sep 2015 15:15:23 +0000 Subject: [PATCH 011/134] IPAM plugin: improve error messages Make it more clear that we failed to find an IPAM plugin. Check for a missing plugin name and issue a more helpful error. --- plugin/ipam.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/ipam.go b/plugin/ipam.go index 93a0d5c9..8690b895 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -63,10 +63,13 @@ func ExecAdd(plugin string, netconf []byte) (*Result, error) { if os.Getenv("CNI_COMMAND") != "ADD" { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } + if plugin == "" { + return nil, fmt.Errorf(`Name of IPAM plugin is missing. Specify a "type" field in the "ipam" section`) + } pluginPath := Find(plugin) if pluginPath == "" { - return nil, fmt.Errorf("could not find %q plugin", plugin) + return nil, fmt.Errorf("could not find %q IPAM plugin", plugin) } stdout := &bytes.Buffer{} From 104e8b483e790485b674f114c5d7291bafc1887d Mon Sep 17 00:00:00 2001 From: Neil Wilson Date: Tue, 8 Sep 2015 03:20:11 +0000 Subject: [PATCH 012/134] plugin/ipam: correct formatting of error message --- plugin/ipam.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/ipam.go b/plugin/ipam.go index 8690b895..f304301f 100644 --- a/plugin/ipam.go +++ b/plugin/ipam.go @@ -64,8 +64,8 @@ func ExecAdd(plugin string, netconf []byte) (*Result, error) { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } if plugin == "" { - return nil, fmt.Errorf(`Name of IPAM plugin is missing. Specify a "type" field in the "ipam" section`) - } + return nil, fmt.Errorf(`name of IPAM plugin is missing. Please specify a "type" field in the "ipam" section`) + } pluginPath := Find(plugin) if pluginPath == "" { From 09a8148e3634e3c267d5ea2f8f2c5beb4eac9dca Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Fri, 7 Aug 2015 16:27:52 +0100 Subject: [PATCH 013/134] Factor an API out into a module This takes some of the machinery from CNI and from the rkt networking code, and turns it into a library that can be linked into go apps. Included is an example command-line application that uses the library, called `cnitool`. Other headline changes: * Plugin exec'ing is factored out The motivation here is to factor out the protocol for invoking plugins. To that end, a generalisation of the code from api.go and pkg/plugin/ipam.go goes into pkg/invoke/exec.go. * Move argument-handling and conf-loading into public API The fact that the arguments get turned into an environment for the plugin is incidental to the API; so, provide a way of supplying them as a struct or saying "just use the same arguments as I got" (the latter is for IPAM plugins). --- invoke/args.go | 76 +++++++++++++++++++ invoke/exec.go | 66 ++++++++++++++++ invoke/find.go | 37 +++++++++ ip/cidr.go | 35 --------- ipam/ipam.go | 75 ++++++++++++++++++ plugin/ipam.go | 151 ------------------------------------- skel/skel.go | 8 +- {plugin => types}/args.go | 16 +++- {plugin => types}/types.go | 54 ++++++++++--- 9 files changed, 316 insertions(+), 202 deletions(-) create mode 100644 invoke/args.go create mode 100644 invoke/exec.go create mode 100644 invoke/find.go create mode 100644 ipam/ipam.go delete mode 100644 plugin/ipam.go rename {plugin => types}/args.go (56%) rename {plugin => types}/types.go (73%) diff --git a/invoke/args.go b/invoke/args.go new file mode 100644 index 00000000..6f0a813a --- /dev/null +++ b/invoke/args.go @@ -0,0 +1,76 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke + +import ( + "os" + "strings" +) + +type CNIArgs interface { + // For use with os/exec; i.e., return nil to inherit the + // environment from this process + AsEnv() []string +} + +type inherited struct{} + +var inheritArgsFromEnv inherited + +func (_ *inherited) AsEnv() []string { + return nil +} + +func ArgsFromEnv() CNIArgs { + return &inheritArgsFromEnv +} + +type Args struct { + Command string + ContainerID string + NetNS string + PluginArgs [][2]string + PluginArgsStr string + IfName string + Path string +} + +func (args *Args) AsEnv() []string { + env := os.Environ() + pluginArgsStr := args.PluginArgsStr + if pluginArgsStr == "" { + pluginArgsStr = stringify(args.PluginArgs) + } + + env = append(env, + "CNI_COMMAND="+args.Command, + "CNI_CONTAINERID="+args.ContainerID, + "CNI_NETNS="+args.NetNS, + "CNI_ARGS="+pluginArgsStr, + "CNI_IFNAME="+args.IfName, + "CNI_PATH="+args.Path) + return env +} + +// taken from rkt/networking/net_plugin.go +func stringify(pluginArgs [][2]string) string { + entries := make([]string, len(pluginArgs)) + + for i, kv := range pluginArgs { + entries[i] = strings.Join(kv[:], "=") + } + + return strings.Join(entries, ";") +} diff --git a/invoke/exec.go b/invoke/exec.go new file mode 100644 index 00000000..d7c5b7ab --- /dev/null +++ b/invoke/exec.go @@ -0,0 +1,66 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/appc/cni/pkg/types" +) + +func pluginErr(err error, output []byte) error { + if _, ok := err.(*exec.ExitError); ok { + emsg := types.Error{} + if perr := json.Unmarshal(output, &emsg); perr != nil { + return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) + } + details := "" + if emsg.Details != "" { + details = fmt.Sprintf("; %v", emsg.Details) + } + return fmt.Errorf("%v%v", emsg.Msg, details) + } + + return err +} + +func ExecPlugin(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { + if pluginPath == "" { + return nil, fmt.Errorf("could not find %q plugin", filepath.Base(pluginPath)) + } + + stdout := &bytes.Buffer{} + + c := exec.Cmd{ + Env: args.AsEnv(), + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stdout: stdout, + Stderr: os.Stderr, + } + if err := c.Run(); err != nil { + return nil, pluginErr(err, stdout.Bytes()) + } + + res := &types.Result{} + err := json.Unmarshal(stdout.Bytes(), res) + return res, err +} diff --git a/invoke/find.go b/invoke/find.go new file mode 100644 index 00000000..dfad12bc --- /dev/null +++ b/invoke/find.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke + +import ( + "os" + "path/filepath" + "strings" +) + +func FindInPath(plugin string, path []string) string { + for _, p := range path { + fullname := filepath.Join(p, plugin) + if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { + return fullname + } + } + return "" +} + +// Find returns the full path of the plugin by searching in CNI_PATH +func Find(plugin string) string { + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + return FindInPath(plugin, paths) +} diff --git a/ip/cidr.go b/ip/cidr.go index c9633988..723a1f74 100644 --- a/ip/cidr.go +++ b/ip/cidr.go @@ -15,23 +15,10 @@ package ip import ( - "encoding/json" "math/big" "net" ) -// ParseCIDR takes a string like "10.2.3.1/24" and -// return IPNet with "10.2.3.1" and /24 mask -func ParseCIDR(s string) (*net.IPNet, error) { - ip, ipn, err := net.ParseCIDR(s) - if err != nil { - return nil, err - } - - ipn.IP = ip - return ipn, nil -} - // NextIP returns IP incremented by 1 func NextIP(ip net.IP) net.IP { i := ipToInt(ip) @@ -62,25 +49,3 @@ func Network(ipn *net.IPNet) *net.IPNet { Mask: ipn.Mask, } } - -// like net.IPNet but adds JSON marshalling and unmarshalling -type IPNet net.IPNet - -func (n IPNet) MarshalJSON() ([]byte, error) { - return json.Marshal((*net.IPNet)(&n).String()) -} - -func (n *IPNet) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - tmp, err := ParseCIDR(s) - if err != nil { - return err - } - - *n = IPNet(*tmp) - return nil -} diff --git a/ipam/ipam.go b/ipam/ipam.go new file mode 100644 index 00000000..a76299d7 --- /dev/null +++ b/ipam/ipam.go @@ -0,0 +1,75 @@ +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipam + +import ( + "fmt" + "os" + + "github.com/appc/cni/pkg/invoke" + "github.com/appc/cni/pkg/ip" + "github.com/appc/cni/pkg/types" + + "github.com/vishvananda/netlink" +) + +func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { + if os.Getenv("CNI_COMMAND") != "ADD" { + return nil, fmt.Errorf("CNI_COMMAND is not ADD") + } + return invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) +} + +func ExecDel(plugin string, netconf []byte) error { + if os.Getenv("CNI_COMMAND") != "DEL" { + return fmt.Errorf("CNI_COMMAND is not DEL") + } + _, err := invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) + return err +} + +// ConfigureIface takes the result of IPAM plugin and +// applies to the ifName interface +func ConfigureIface(ifName string, res *types.Result) error { + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed to set %q UP: %v", ifName, err) + } + + // TODO(eyakubovich): IPv6 + addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) + } + + for _, r := range res.IP4.Routes { + gw := r.GW + if gw == nil { + gw = res.IP4.Gateway + } + if err = ip.AddRoute(&r.Dst, gw, link); err != nil { + // we skip over duplicate routes as we assume the first one wins + if !os.IsExist(err) { + return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) + } + } + } + + return nil +} diff --git a/plugin/ipam.go b/plugin/ipam.go deleted file mode 100644 index f304301f..00000000 --- a/plugin/ipam.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package plugin - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/appc/cni/pkg/ip" - "github.com/vishvananda/netlink" -) - -// Find returns the full path of the plugin by searching in CNI_PATH -func Find(plugin string) string { - paths := strings.Split(os.Getenv("CNI_PATH"), ":") - - for _, p := range paths { - fullname := filepath.Join(p, plugin) - if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { - return fullname - } - } - - return "" -} - -func pluginErr(err error, output []byte) error { - if _, ok := err.(*exec.ExitError); ok { - emsg := Error{} - if perr := json.Unmarshal(output, &emsg); perr != nil { - return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) - } - details := "" - if emsg.Details != "" { - details = fmt.Sprintf("; %v", emsg.Details) - } - return fmt.Errorf("%v%v", emsg.Msg, details) - } - - return err -} - -// ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD. -// Parses and returns resulting IPConfig -func ExecAdd(plugin string, netconf []byte) (*Result, error) { - if os.Getenv("CNI_COMMAND") != "ADD" { - return nil, fmt.Errorf("CNI_COMMAND is not ADD") - } - if plugin == "" { - return nil, fmt.Errorf(`name of IPAM plugin is missing. Please specify a "type" field in the "ipam" section`) - } - - pluginPath := Find(plugin) - if pluginPath == "" { - return nil, fmt.Errorf("could not find %q IPAM plugin", plugin) - } - - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(netconf), - Stdout: stdout, - Stderr: os.Stderr, - } - if err := c.Run(); err != nil { - return nil, pluginErr(err, stdout.Bytes()) - } - - res := &Result{} - err := json.Unmarshal(stdout.Bytes(), res) - return res, err -} - -// ExecDel executes IPAM plugin, assuming CNI_COMMAND == DEL. -func ExecDel(plugin string, netconf []byte) error { - if os.Getenv("CNI_COMMAND") != "DEL" { - return fmt.Errorf("CNI_COMMAND is not DEL") - } - - pluginPath := Find(plugin) - if pluginPath == "" { - return fmt.Errorf("could not find %q plugin", plugin) - } - - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(netconf), - Stdout: stdout, - Stderr: os.Stderr, - } - if err := c.Run(); err != nil { - return pluginErr(err, stdout.Bytes()) - } - return nil -} - -// ConfigureIface takes the result of IPAM plugin and -// applies to the ifName interface -func ConfigureIface(ifName string, res *Result) error { - link, err := netlink.LinkByName(ifName) - if err != nil { - return fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - if err := netlink.LinkSetUp(link); err != nil { - return fmt.Errorf("failed to set %q UP: %v", ifName, err) - } - - // TODO(eyakubovich): IPv6 - addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} - if err = netlink.AddrAdd(link, addr); err != nil { - return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) - } - - for _, r := range res.IP4.Routes { - gw := r.GW - if gw == nil { - gw = res.IP4.Gateway - } - if err = ip.AddRoute(&r.Dst, gw, link); err != nil { - // we skip over duplicate routes as we assume the first one wins - if !os.IsExist(err) { - return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) - } - } - } - - return nil -} diff --git a/skel/skel.go b/skel/skel.go index bf79b91d..d6204dd4 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -22,7 +22,7 @@ import ( "log" "os" - "github.com/appc/cni/pkg/plugin" + "github.com/appc/cni/pkg/types" ) // CmdArgs captures all the arguments passed in to the plugin @@ -93,7 +93,7 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } if err != nil { - if e, ok := err.(*plugin.Error); ok { + if e, ok := err.(*types.Error); ok { // don't wrap Error in Error dieErr(e) } @@ -102,14 +102,14 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { } func dieMsg(f string, args ...interface{}) { - e := &plugin.Error{ + e := &types.Error{ Code: 100, Msg: fmt.Sprintf(f, args...), } dieErr(e) } -func dieErr(e *plugin.Error) { +func dieErr(e *types.Error) { if err := e.Print(); err != nil { log.Print("Error writing error JSON to stdout: ", err) } diff --git a/plugin/args.go b/types/args.go similarity index 56% rename from plugin/args.go rename to types/args.go index 274ec66b..68162435 100644 --- a/plugin/args.go +++ b/types/args.go @@ -1,4 +1,18 @@ -package plugin +// Copyright 2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types import ( "encoding" diff --git a/plugin/types.go b/types/types.go similarity index 73% rename from plugin/types.go rename to types/types.go index d5952dde..21ba32d6 100644 --- a/plugin/types.go +++ b/types/types.go @@ -12,16 +12,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -package plugin +package types import ( "encoding/json" "net" "os" - - "github.com/appc/cni/pkg/ip" ) +// like net.IPNet but adds JSON marshalling and unmarshalling +type IPNet net.IPNet + +// ParseCIDR takes a string like "10.2.3.1/24" and +// return IPNet with "10.2.3.1" and /24 mask +func ParseCIDR(s string) (*net.IPNet, error) { + ip, ipn, err := net.ParseCIDR(s) + if err != nil { + return nil, err + } + + ipn.IP = ip + return ipn, nil +} + +func (n IPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(&n).String()) +} + +func (n *IPNet) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + tmp, err := ParseCIDR(s) + if err != nil { + return err + } + + *n = IPNet(*tmp) + return nil +} + // NetConf describes a network. type NetConf struct { Name string `json:"name,omitempty"` @@ -68,23 +100,23 @@ func (e *Error) Print() error { } // net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom ip.IPNet type +// for our custom IPNet type // JSON (un)marshallable types type ipConfig struct { - IP ip.IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []Route `json:"routes,omitempty"` + IP IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []Route `json:"routes,omitempty"` } type route struct { - Dst ip.IPNet `json:"dst"` - GW net.IP `json:"gw,omitempty"` + Dst IPNet `json:"dst"` + GW net.IP `json:"gw,omitempty"` } func (c *IPConfig) MarshalJSON() ([]byte, error) { ipc := ipConfig{ - IP: ip.IPNet(c.IP), + IP: IPNet(c.IP), Gateway: c.Gateway, Routes: c.Routes, } @@ -117,7 +149,7 @@ func (r *Route) UnmarshalJSON(data []byte) error { func (r *Route) MarshalJSON() ([]byte, error) { rt := route{ - Dst: ip.IPNet(r.Dst), + Dst: IPNet(r.Dst), GW: r.GW, } From ae3ceedd69cad73fa69850e88352f584952bf361 Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Fri, 18 Sep 2015 10:30:10 -0700 Subject: [PATCH 014/134] bug fix: exec of DEL cmd caused JSON decode error When plugin is executed with a DEL command, it does not print result to stdout unless there is an error. Therefore it stdout bytes should not be passed to json.Unmarshal. --- invoke/exec.go | 22 ++++++++++++++++++---- ipam/ipam.go | 5 ++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/invoke/exec.go b/invoke/exec.go index d7c5b7ab..cf0cff47 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -41,7 +41,23 @@ func pluginErr(err error, output []byte) error { return err } -func ExecPlugin(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { +func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { + stdoutBytes, err := execPlugin(pluginPath, netconf, args) + if err != nil { + return nil, err + } + + res := &types.Result{} + err = json.Unmarshal(stdoutBytes, res) + return res, err +} + +func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { + _, err := execPlugin(pluginPath, netconf, args) + return err +} + +func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) { if pluginPath == "" { return nil, fmt.Errorf("could not find %q plugin", filepath.Base(pluginPath)) } @@ -60,7 +76,5 @@ func ExecPlugin(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, return nil, pluginErr(err, stdout.Bytes()) } - res := &types.Result{} - err := json.Unmarshal(stdout.Bytes(), res) - return res, err + return stdout.Bytes(), nil } diff --git a/ipam/ipam.go b/ipam/ipam.go index a76299d7..6a593a26 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -29,15 +29,14 @@ func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { if os.Getenv("CNI_COMMAND") != "ADD" { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } - return invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) + return invoke.ExecPluginWithResult(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) } func ExecDel(plugin string, netconf []byte) error { if os.Getenv("CNI_COMMAND") != "DEL" { return fmt.Errorf("CNI_COMMAND is not DEL") } - _, err := invoke.ExecPlugin(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) - return err + return invoke.ExecPluginWithoutResult(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) } // ConfigureIface takes the result of IPAM plugin and From 8d3eb91a374b7b66479404be4acd9b56ab4f93eb Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 23 Sep 2015 11:03:22 +0200 Subject: [PATCH 015/134] plugins/ptp: allow host veth to be UP --- ip/link.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ip/link.go b/ip/link.go index 3936ed9d..e97dcd28 100644 --- a/ip/link.go +++ b/ip/link.go @@ -77,8 +77,9 @@ func RandomVethName() (string, error) { return fmt.Sprintf("veth%x", entropy), nil } -// SetupVeth sets up a virtual ethernet link. -// Should be in container netns. +// SetupVeth creates the virtual ethernet pair and sets up the container's end in the container netns. +// Setting up the host end up has to be done in the host netns outside of this function. +// This is because moving the host veth end will cause it to be brought down automatically when it is moved to the host netns. func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { var hostVethName string hostVethName, contVeth, err = makeVeth(contVethName, mtu) @@ -97,11 +98,6 @@ func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVet return } - if err = netlink.LinkSetUp(hostVeth); err != nil { - err = fmt.Errorf("failed to set %q up: %v", contVethName, err) - return - } - if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { err = fmt.Errorf("failed to move veth to host netns: %v", err) return From 8525fef4664d1104c58f2afa32d4e0b729f088c3 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 24 Sep 2015 18:28:14 +0200 Subject: [PATCH 016/134] Revert "plugins/ptp: allow host veth to be UP" This reverts commit 231d2d5a27f3ba54219c3f0b1c8ef2c5dab4faaf. --- ip/link.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ip/link.go b/ip/link.go index e97dcd28..3936ed9d 100644 --- a/ip/link.go +++ b/ip/link.go @@ -77,9 +77,8 @@ func RandomVethName() (string, error) { return fmt.Sprintf("veth%x", entropy), nil } -// SetupVeth creates the virtual ethernet pair and sets up the container's end in the container netns. -// Setting up the host end up has to be done in the host netns outside of this function. -// This is because moving the host veth end will cause it to be brought down automatically when it is moved to the host netns. +// SetupVeth sets up a virtual ethernet link. +// Should be in container netns. func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { var hostVethName string hostVethName, contVeth, err = makeVeth(contVethName, mtu) @@ -98,6 +97,11 @@ func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVet return } + if err = netlink.LinkSetUp(hostVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", contVethName, err) + return + } + if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { err = fmt.Errorf("failed to move veth to host netns: %v", err) return From 4da892a6696977fc74c64271eb93fdcfe405039d Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 24 Sep 2015 18:27:41 +0200 Subject: [PATCH 017/134] link: switch to host netns to set up host veth end --- ip/link.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ip/link.go b/ip/link.go index 3936ed9d..44e5d84d 100644 --- a/ip/link.go +++ b/ip/link.go @@ -20,6 +20,7 @@ import ( "net" "os" + "github.com/appc/cni/pkg/ns" "github.com/vishvananda/netlink" ) @@ -78,7 +79,8 @@ func RandomVethName() (string, error) { } // SetupVeth sets up a virtual ethernet link. -// Should be in container netns. +// Should be in container netns, and will switch back to hostNS to set the host +// veth end up. func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { var hostVethName string hostVethName, contVeth, err = makeVeth(contVethName, mtu) @@ -97,16 +99,22 @@ func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVet return } - if err = netlink.LinkSetUp(hostVeth); err != nil { - err = fmt.Errorf("failed to set %q up: %v", contVethName, err) - return - } - if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { err = fmt.Errorf("failed to move veth to host netns: %v", err) return } + err = ns.WithNetNS(hostNS, false, func(_ *os.File) error { + hostVeth, err := netlink.LinkByName(hostVethName) + if err != nil { + return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Name(), err) + } + + if err = netlink.LinkSetUp(hostVeth); err != nil { + return fmt.Errorf("failed to set %q up: %v", hostVethName, err) + } + return nil + }) return } From 4bee884fae4d4ed5f759402ca385d7ca563cb944 Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Tue, 29 Sep 2015 11:51:33 -0700 Subject: [PATCH 018/134] Change copyright from CoreOS to CNI authors CNI is developed by more than just CoreOS, Inc and the copyright is retained by all CNI contributors (for their respective contributed code). --- invoke/args.go | 2 +- invoke/exec.go | 2 +- invoke/find.go | 2 +- ip/cidr.go | 2 +- ip/ipforward.go | 2 +- ip/ipmasq.go | 2 +- ip/link.go | 2 +- ip/route.go | 2 +- ipam/ipam.go | 2 +- ns/ns.go | 2 +- skel/skel.go | 2 +- types/args.go | 2 +- types/types.go | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/invoke/args.go b/invoke/args.go index 6f0a813a..be28ba62 100644 --- a/invoke/args.go +++ b/invoke/args.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/invoke/exec.go b/invoke/exec.go index cf0cff47..90564970 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/invoke/find.go b/invoke/find.go index dfad12bc..82f9a9b7 100644 --- a/invoke/find.go +++ b/invoke/find.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ip/cidr.go b/ip/cidr.go index 723a1f74..dae2c4d0 100644 --- a/ip/cidr.go +++ b/ip/cidr.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ip/ipforward.go b/ip/ipforward.go index 0a1ca252..77ee7463 100644 --- a/ip/ipforward.go +++ b/ip/ipforward.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ip/ipmasq.go b/ip/ipmasq.go index 7f8740df..44b7edf3 100644 --- a/ip/ipmasq.go +++ b/ip/ipmasq.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ip/link.go b/ip/link.go index 44e5d84d..1ba529da 100644 --- a/ip/link.go +++ b/ip/link.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ip/route.go b/ip/route.go index 04e660ef..6c8658b2 100644 --- a/ip/route.go +++ b/ip/route.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ipam/ipam.go b/ipam/ipam.go index 6a593a26..4920838a 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/ns/ns.go b/ns/ns.go index 20548b9b..9996aac7 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/skel/skel.go b/skel/skel.go index d6204dd4..5c3532db 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -1,4 +1,4 @@ -// Copyright 2014 CoreOS, Inc. +// Copyright 2014 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/types/args.go b/types/args.go index 68162435..3c0fd88e 100644 --- a/types/args.go +++ b/types/args.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/types/types.go b/types/types.go index 21ba32d6..7fce5b82 100644 --- a/types/types.go +++ b/types/types.go @@ -1,4 +1,4 @@ -// Copyright 2015 CoreOS, Inc. +// Copyright 2015 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 6b58eda23a3bb521735ea03c1833125dbd29c4c3 Mon Sep 17 00:00:00 2001 From: Derek Gonyeo Date: Wed, 16 Dec 2015 13:20:19 -0800 Subject: [PATCH 019/134] pkg/ip: fix typo in error message, s/iptabes/iptables/ --- ip/ipmasq.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ip/ipmasq.go b/ip/ipmasq.go index 44b7edf3..6901f69e 100644 --- a/ip/ipmasq.go +++ b/ip/ipmasq.go @@ -26,7 +26,7 @@ import ( func SetupIPMasq(ipn *net.IPNet, chain string) error { ipt, err := iptables.New() if err != nil { - return fmt.Errorf("failed to locate iptabes: %v", err) + return fmt.Errorf("failed to locate iptables: %v", err) } if err = ipt.NewChain("nat", chain); err != nil { @@ -51,7 +51,7 @@ func SetupIPMasq(ipn *net.IPNet, chain string) error { func TeardownIPMasq(ipn *net.IPNet, chain string) error { ipt, err := iptables.New() if err != nil { - return fmt.Errorf("failed to locate iptabes: %v", err) + return fmt.Errorf("failed to locate iptables: %v", err) } if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain); err != nil { From 3bbc43f24de46d64bab1c666c956081ab4e235b2 Mon Sep 17 00:00:00 2001 From: Alban Crequy Date: Tue, 26 Jan 2016 18:54:56 +0100 Subject: [PATCH 020/134] *: add "dns" field to the configuration appc/cni#76 added a "dns" field in the result JSON. But before this patch, the plugins had no way of knowing which name server to return. There could be two ways of knowing which name server to return: 1. add it as an extra argument ("CNI_ARGS") 2. add it in the network configuration as a convenience (received via stdin) I chose the second way because it is easier. In the case of rkt, it means the user could just add the DNS name servers in /etc/rkt/net.d/mynetwork.conf. --- types/types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/types/types.go b/types/types.go index 7fce5b82..e5558be4 100644 --- a/types/types.go +++ b/types/types.go @@ -67,6 +67,7 @@ type NetConf struct { type Result struct { IP4 *IPConfig `json:"ip4,omitempty"` IP6 *IPConfig `json:"ip6,omitempty"` + DNS []string `json:"dns,omitempty"` } func (r *Result) Print() error { From a08b015fa52d7ee1672e076f286414a3f1f42036 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 27 Jan 2016 12:12:16 +0100 Subject: [PATCH 021/134] *: reflect SPEC's DNS changes in implementation * DNS is now a type which will result in a JSON dictionary in configurations and results * Minor refactoring, making use of type embedding --- types/types.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/types/types.go b/types/types.go index e5558be4..6ae4a644 100644 --- a/types/types.go +++ b/types/types.go @@ -61,13 +61,14 @@ type NetConf struct { IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` + DNS DNS `json:"dns"` } // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { IP4 *IPConfig `json:"ip4,omitempty"` IP6 *IPConfig `json:"ip6,omitempty"` - DNS []string `json:"dns,omitempty"` + DNS DNS `json:"dns,omitempty"` } func (r *Result) Print() error { @@ -81,6 +82,14 @@ type IPConfig struct { Routes []Route } +// DNS contains values interesting for DNS resolvers +type DNS struct { + Nameservers []string `json:"nameservers,omitempty"` + Domain string `json:"domain,omitempty"` + Search []string `json:"search,omitempty"` + Options []string `json:"options,omitempty"` +} + type Route struct { Dst net.IPNet GW net.IP From 3382b459a71eeb855fc815f95662fe1285ea51df Mon Sep 17 00:00:00 2001 From: Zachary Gershman Date: Wed, 10 Feb 2016 13:42:10 -0800 Subject: [PATCH 022/134] Better error message when plugin cannot be found --- invoke/exec.go | 5 --- invoke/find.go | 34 +++++++++++++------- invoke/find_test.go | 64 +++++++++++++++++++++++++++++++++++++ invoke/invoke_suite_test.go | 13 ++++++++ ipam/ipam.go | 21 ++++++++++-- 5 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 invoke/find_test.go create mode 100644 invoke/invoke_suite_test.go diff --git a/invoke/exec.go b/invoke/exec.go index 90564970..337bfcb8 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "github.com/appc/cni/pkg/types" ) @@ -58,10 +57,6 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) er } func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) { - if pluginPath == "" { - return nil, fmt.Errorf("could not find %q plugin", filepath.Base(pluginPath)) - } - stdout := &bytes.Buffer{} c := exec.Cmd{ diff --git a/invoke/find.go b/invoke/find.go index 82f9a9b7..3b037907 100644 --- a/invoke/find.go +++ b/invoke/find.go @@ -15,23 +15,33 @@ package invoke import ( + "fmt" "os" "path/filepath" - "strings" ) -func FindInPath(plugin string, path []string) string { - for _, p := range path { - fullname := filepath.Join(p, plugin) - if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { - return fullname +// FindInPath returns the full path of the plugin by searching in the provided path +func FindInPath(plugin string, paths []string) (string, error) { + if plugin == "" { + return "", fmt.Errorf("no plugin name provided") + } + + if len(paths) == 0 { + return "", fmt.Errorf("no paths provided") + } + + var fullpath string + for _, path := range paths { + full := filepath.Join(path, plugin) + if fi, err := os.Stat(full); err == nil && fi.Mode().IsRegular() { + fullpath = full + break } } - return "" -} -// Find returns the full path of the plugin by searching in CNI_PATH -func Find(plugin string) string { - paths := strings.Split(os.Getenv("CNI_PATH"), ":") - return FindInPath(plugin, paths) + if fullpath == "" { + return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths) + } + + return fullpath, nil } diff --git a/invoke/find_test.go b/invoke/find_test.go new file mode 100644 index 00000000..00baa313 --- /dev/null +++ b/invoke/find_test.go @@ -0,0 +1,64 @@ +package invoke_test + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/appc/cni/pkg/invoke" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("FindInPath", func() { + var ( + multiplePaths []string + pluginName string + pluginDir string + anotherTempDir string + ) + + BeforeEach(func() { + tempDir, err := ioutil.TempDir("", "cni-find") + Expect(err).NotTo(HaveOccurred()) + + plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin") + + anotherTempDir, err = ioutil.TempDir("", "nothing-here") + + multiplePaths = []string{anotherTempDir, tempDir} + pluginDir, pluginName = filepath.Split(plugin.Name()) + }) + + Context("when multiple paths are provided", func() { + It("returns only the path to the plugin", func() { + pluginPath, err := invoke.FindInPath(pluginName, multiplePaths) + Expect(err).NotTo(HaveOccurred()) + Expect(pluginPath).To(Equal(filepath.Join(pluginDir, pluginName))) + }) + }) + + Context("when an error occurs", func() { + Context("when no paths are provided", func() { + It("returns an error noting no paths were provided", func() { + _, err := invoke.FindInPath(pluginName, []string{}) + Expect(err).To(MatchError("no paths provided")) + }) + }) + + Context("when no plugin is provided", func() { + It("returns an error noting the plugin name wasn't found", func() { + _, err := invoke.FindInPath("", multiplePaths) + Expect(err).To(MatchError("no plugin name provided")) + }) + }) + + Context("when the plugin cannot be found", func() { + It("returns an error noting the path", func() { + pathsWithNothing := []string{anotherTempDir} + _, err := invoke.FindInPath(pluginName, pathsWithNothing) + Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing))) + }) + }) + }) +}) diff --git a/invoke/invoke_suite_test.go b/invoke/invoke_suite_test.go new file mode 100644 index 00000000..e570c964 --- /dev/null +++ b/invoke/invoke_suite_test.go @@ -0,0 +1,13 @@ +package invoke_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestInvoke(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Invoke Suite") +} diff --git a/ipam/ipam.go b/ipam/ipam.go index 4920838a..1d512ecd 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -17,6 +17,7 @@ package ipam import ( "fmt" "os" + "strings" "github.com/appc/cni/pkg/invoke" "github.com/appc/cni/pkg/ip" @@ -29,14 +30,30 @@ func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { if os.Getenv("CNI_COMMAND") != "ADD" { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } - return invoke.ExecPluginWithResult(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) + + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + pluginPath, err := invoke.FindInPath(plugin, paths) + if err != nil { + return nil, err + } + + return invoke.ExecPluginWithResult(pluginPath, netconf, invoke.ArgsFromEnv()) } func ExecDel(plugin string, netconf []byte) error { if os.Getenv("CNI_COMMAND") != "DEL" { return fmt.Errorf("CNI_COMMAND is not DEL") } - return invoke.ExecPluginWithoutResult(invoke.Find(plugin), netconf, invoke.ArgsFromEnv()) + + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + pluginPath, err := invoke.FindInPath(plugin, paths) + if err != nil { + return err + } + + return invoke.ExecPluginWithoutResult(pluginPath, netconf, invoke.ArgsFromEnv()) } // ConfigureIface takes the result of IPAM plugin and From d2af99534945f4130cf0e5822b024f5cf24e0230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Martins?= Date: Sun, 31 Jan 2016 03:18:33 +0000 Subject: [PATCH 023/134] added the String method to Result type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Martins --- types/types.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/types/types.go b/types/types.go index 6ae4a644..6948dcb1 100644 --- a/types/types.go +++ b/types/types.go @@ -16,6 +16,7 @@ package types import ( "encoding/json" + "fmt" "net" "os" ) @@ -75,6 +76,20 @@ func (r *Result) Print() error { return prettyPrint(r) } +// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where +// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the +// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. +func (r *Result) String() string { + var str string + if r.IP4 != nil { + str = fmt.Sprintf("IP4:%+v, ", *r.IP4) + } + if r.IP6 != nil { + str += fmt.Sprintf("IP6:%+v, ", *r.IP6) + } + return fmt.Sprintf("%sDNS:%+v", str, r.DNS) +} + // IPConfig contains values necessary to configure an interface type IPConfig struct { IP net.IPNet From 05683bf11d49eeeb046a595324022b2b69459f15 Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Wed, 2 Mar 2016 17:02:52 +0000 Subject: [PATCH 024/134] Add invoke.Delegate{Add,Del} for use by meta-plugins The 'flannel' meta plugin delegates to other plugins to do the actual OS-level work. It used the ipam.Exec{Add,Del} procedures for this delegation, since those do precisely what's needed. However this is a bit misleading, since the flannel plugin _isn't_ doing this for IPAM, and the ipam.Exec* procedures aren't doing something specific to IPAM plugins. So: anticipating that there may be more meta plugins that want to delegate in the same way, this commit moves generic delegation procedures to `pkg/invoke`, and makes the `pkg/ipam` procedures (still used, accurately, in the non-meta plugins) shims. --- invoke/delegate.go | 39 +++++++++++++++++++++++++++++++++++++++ ipam/ipam.go | 27 ++------------------------- 2 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 invoke/delegate.go diff --git a/invoke/delegate.go b/invoke/delegate.go new file mode 100644 index 00000000..9864e6f5 --- /dev/null +++ b/invoke/delegate.go @@ -0,0 +1,39 @@ +package invoke + +import ( + "fmt" + "os" + "strings" + + "github.com/appc/cni/pkg/types" +) + +func DelegateAdd(delegatePlugin string, netconf []byte) (*types.Result, error) { + if os.Getenv("CNI_COMMAND") != "ADD" { + return nil, fmt.Errorf("CNI_COMMAND is not ADD") + } + + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + pluginPath, err := FindInPath(delegatePlugin, paths) + if err != nil { + return nil, err + } + + return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv()) +} + +func DelegateDel(delegatePlugin string, netconf []byte) error { + if os.Getenv("CNI_COMMAND") != "DEL" { + return fmt.Errorf("CNI_COMMAND is not DEL") + } + + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + pluginPath, err := FindInPath(delegatePlugin, paths) + if err != nil { + return err + } + + return ExecPluginWithoutResult(pluginPath, netconf, ArgsFromEnv()) +} diff --git a/ipam/ipam.go b/ipam/ipam.go index 1d512ecd..f0adfb7f 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -17,7 +17,6 @@ package ipam import ( "fmt" "os" - "strings" "github.com/appc/cni/pkg/invoke" "github.com/appc/cni/pkg/ip" @@ -27,33 +26,11 @@ import ( ) func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { - if os.Getenv("CNI_COMMAND") != "ADD" { - return nil, fmt.Errorf("CNI_COMMAND is not ADD") - } - - paths := strings.Split(os.Getenv("CNI_PATH"), ":") - - pluginPath, err := invoke.FindInPath(plugin, paths) - if err != nil { - return nil, err - } - - return invoke.ExecPluginWithResult(pluginPath, netconf, invoke.ArgsFromEnv()) + return invoke.DelegateAdd(plugin, netconf) } func ExecDel(plugin string, netconf []byte) error { - if os.Getenv("CNI_COMMAND") != "DEL" { - return fmt.Errorf("CNI_COMMAND is not DEL") - } - - paths := strings.Split(os.Getenv("CNI_PATH"), ":") - - pluginPath, err := invoke.FindInPath(plugin, paths) - if err != nil { - return err - } - - return invoke.ExecPluginWithoutResult(pluginPath, netconf, invoke.ArgsFromEnv()) + return invoke.DelegateDel(plugin, netconf) } // ConfigureIface takes the result of IPAM plugin and From b99854d124d7ee5a2e630a846bb8609c427b6f45 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Mon, 14 Mar 2016 18:57:16 -0700 Subject: [PATCH 025/134] WithNetNS restores original namespace when callback errors - adds test coverage of WithNetNS in BDD-style --- ns/ns.go | 4 +- ns/ns_suite_test.go | 20 ++++++ ns/ns_test.go | 153 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 ns/ns_suite_test.go create mode 100644 ns/ns_test.go diff --git a/ns/ns.go b/ns/ns.go index 9996aac7..97c6a1e9 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -82,11 +82,11 @@ func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error { if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil { return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err) } + defer SetNS(thisNS, syscall.CLONE_NEWNET) // switch back if err = f(thisNS); err != nil { return err } - // switch back - return SetNS(thisNS, syscall.CLONE_NEWNET) + return nil } diff --git a/ns/ns_suite_test.go b/ns/ns_suite_test.go new file mode 100644 index 00000000..ff26ec2e --- /dev/null +++ b/ns/ns_suite_test.go @@ -0,0 +1,20 @@ +package ns_test + +import ( + "math/rand" + "runtime" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/gomega" + + "testing" +) + +func TestNs(t *testing.T) { + rand.Seed(config.GinkgoConfig.RandomSeed) + runtime.LockOSThread() + + RegisterFailHandler(Fail) + RunSpecs(t, "pkg/ns Suite") +} diff --git a/ns/ns_test.go b/ns/ns_test.go new file mode 100644 index 00000000..ad52f7f4 --- /dev/null +++ b/ns/ns_test.go @@ -0,0 +1,153 @@ +package ns_test + +import ( + "errors" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + + "golang.org/x/sys/unix" + + "github.com/appc/cni/pkg/ns" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func getInode(path string) (uint64, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + return getInodeF(file) +} + +func getInodeF(file *os.File) (uint64, error) { + stat := &unix.Stat_t{} + err := unix.Fstat(int(file.Fd()), stat) + return stat.Ino, err +} + +const CurrentNetNS = "/proc/self/ns/net" + +var _ = Describe("Linux namespace operations", func() { + Describe("WithNetNS", func() { + var ( + originalNetNS *os.File + + targetNetNSName string + targetNetNSPath string + targetNetNS *os.File + ) + + BeforeEach(func() { + var err error + originalNetNS, err = os.Open(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + targetNetNSName = fmt.Sprintf("test-netns-%d", rand.Int()) + + err = exec.Command("ip", "netns", "add", targetNetNSName).Run() + Expect(err).NotTo(HaveOccurred()) + + targetNetNSPath = filepath.Join("/var/run/netns/", targetNetNSName) + targetNetNS, err = os.Open(targetNetNSPath) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(targetNetNS.Close()).To(Succeed()) + + err := exec.Command("ip", "netns", "del", targetNetNSName).Run() + Expect(err).NotTo(HaveOccurred()) + + Expect(originalNetNS.Close()).To(Succeed()) + }) + + It("executes the callback within the target network namespace", func() { + expectedInode, err := getInode(targetNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + var actualInode uint64 + var innerErr error + err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { + actualInode, innerErr = getInode(CurrentNetNS) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(innerErr).NotTo(HaveOccurred()) + Expect(actualInode).To(Equal(expectedInode)) + }) + + It("provides the original namespace as the argument to the callback", func() { + hostNSInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + var inputNSInode uint64 + var innerErr error + err = ns.WithNetNS(targetNetNS, false, func(inputNS *os.File) error { + inputNSInode, err = getInodeF(inputNS) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(innerErr).NotTo(HaveOccurred()) + Expect(inputNSInode).To(Equal(hostNSInode)) + }) + + It("restores the calling thread to the original network namespace", func() { + preTestInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + postTestInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + Expect(postTestInode).To(Equal(preTestInode)) + }) + + Context("when the callback returns an error", func() { + It("restores the calling thread to the original namespace before returning", func() { + preTestInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { + return errors.New("potato") + }) + + postTestInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + Expect(postTestInode).To(Equal(preTestInode)) + }) + + It("returns the error from the callback", func() { + err := ns.WithNetNS(targetNetNS, false, func(*os.File) error { + return errors.New("potato") + }) + Expect(err).To(MatchError("potato")) + }) + }) + + Describe("validating inode mapping to namespaces", func() { + It("checks that different namespaces have different inodes", func() { + hostNSInode, err := getInode(CurrentNetNS) + Expect(err).NotTo(HaveOccurred()) + + testNsInode, err := getInode(targetNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(hostNSInode).NotTo(Equal(0)) + Expect(testNsInode).NotTo(Equal(0)) + Expect(testNsInode).NotTo(Equal(hostNSInode)) + }) + }) + }) +}) From ca978caee7a39cfbfe2b5fdbbfa1942a561d94a2 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 15 Mar 2016 08:37:00 -0700 Subject: [PATCH 026/134] Update docstring on WithNetNS --- ns/ns.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ns/ns.go b/ns/ns.go index 97c6a1e9..4f0814f1 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -66,7 +66,8 @@ func WithNetNSPath(nspath string, lockThread bool, f func(*os.File) error) error // Changing namespaces must be done on a goroutine that has been // locked to an OS thread. If lockThread arg is true, this function // locks the goroutine prior to change namespace and unlocks before -// returning +// returning. If the closure returns an error, WithNetNS attempts to +// restore the original namespace before returning. func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error { if lockThread { runtime.LockOSThread() From 8cc424c5aa959396f62489c152b7b280dc7d4e32 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 18 Mar 2016 22:28:14 +0100 Subject: [PATCH 027/134] pkg/skel: add rudimentary unit tests This is an attempt to testing the PluginMain() function of the skel pkg. We should be able to do better by using a mockable interface for the plugins, but this is a start. --- skel/skel_suite_test.go | 13 +++++++++ skel/skel_test.go | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 skel/skel_suite_test.go create mode 100644 skel/skel_test.go diff --git a/skel/skel_suite_test.go b/skel/skel_suite_test.go new file mode 100644 index 00000000..4820d5d9 --- /dev/null +++ b/skel/skel_suite_test.go @@ -0,0 +1,13 @@ +package skel + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestSkel(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Skel Suite") +} diff --git a/skel/skel_test.go b/skel/skel_test.go new file mode 100644 index 00000000..c6fb4e80 --- /dev/null +++ b/skel/skel_test.go @@ -0,0 +1,61 @@ +package skel + +import ( + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Skel", func() { + var ( + fNoop = func(_ *CmdArgs) error { return nil } + // fErr = func(_ *CmdArgs) error { return errors.New("dummy") } + envVars = []struct { + name string + val string + }{ + {"CNI_CONTAINERID", "dummy"}, + {"CNI_NETNS", "dummy"}, + {"CNI_IFNAME", "dummy"}, + {"CNI_ARGS", "dummy"}, + {"CNI_PATH", "dummy"}, + } + ) + + It("Must be possible to set the env vars", func() { + for _, v := range envVars { + err := os.Setenv(v.name, v.val) + Expect(err).NotTo(HaveOccurred()) + } + }) + + Context("When dummy environment variables are passed", func() { + + It("should not fail with ADD and noop callback", func() { + err := os.Setenv("CNI_COMMAND", "ADD") + Expect(err).NotTo(HaveOccurred()) + PluginMain(fNoop, nil) + }) + + // TODO: figure out howto mock printing and os.Exit() + // It("should fail with ADD and error callback", func() { + // err := os.Setenv("CNI_COMMAND", "ADD") + // Expect(err).NotTo(HaveOccurred()) + // PluginMain(fErr, nil) + // }) + + It("should not fail with DEL and noop callback", func() { + err := os.Setenv("CNI_COMMAND", "DEL") + Expect(err).NotTo(HaveOccurred()) + PluginMain(nil, fNoop) + }) + + // TODO: figure out howto mock printing and os.Exit() + // It("should fail with DEL and error callback", func() { + // err := os.Setenv("CNI_COMMAND", "DEL") + // Expect(err).NotTo(HaveOccurred()) + // PluginMain(fErr, nil) + // }) + }) +}) From fc83c24eb5013f710b9495cc300ce2887e870601 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 17 Mar 2016 14:08:29 +0100 Subject: [PATCH 028/134] pkg/types: add IgnoreUnknown arg and logic This commit adds a struct type CommonArgs that is to be embedded in every plugin's argument struct. It contains a field named "IgnoreUnknown" which will be parsed as a boolean and can be provided to ignore unknown arguments passed to the plugin. --- types/args.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/types/args.go b/types/args.go index 3c0fd88e..c4e3c55b 100644 --- a/types/args.go +++ b/types/args.go @@ -21,6 +21,39 @@ import ( "strings" ) +// UnmarshallableBool typedef for builtin bool +// because builtin type's methods can't be declared +type UnmarshallableBool bool + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Returns boolean true if the string is "1" or "[Tt]rue" +// Returns boolean false if the string is "0" or "[Ff]alse" +func (b *UnmarshallableBool) UnmarshalText(data []byte) error { + s := strings.ToLower(string(data)) + switch s { + case "1", "true": + *b = true + case "0", "false": + *b = false + default: + return fmt.Errorf("Boolean unmarshal error: invalid input %s", s) + } + return nil +} + +// CommonArgs contains the IgnoreUnknown argument +// and must be embedded by all Arg structs +type CommonArgs struct { + IgnoreUnknown UnmarshallableBool `json:"ignoreunknown,omitempty"` +} + +// getKeyField is a helper function to receive Values +// Values that represent a pointer to a struct +func getKeyField(keyString string, v reflect.Value) reflect.Value { + return v.Elem().FieldByName(keyString) +} + +// LoadArgs parses args from a string in the form "K=V;K2=V2;..." func LoadArgs(args string, container interface{}) error { if args == "" { return nil @@ -29,6 +62,7 @@ func LoadArgs(args string, container interface{}) error { containerValue := reflect.ValueOf(container) pairs := strings.Split(args, ";") + unknownArgs := []string{} for _, pair := range pairs { kv := strings.Split(pair, "=") if len(kv) != 2 { @@ -36,15 +70,22 @@ func LoadArgs(args string, container interface{}) error { } keyString := kv[0] valueString := kv[1] - keyField := containerValue.Elem().FieldByName(keyString) + keyField := GetKeyField(keyString, containerValue) if !keyField.IsValid() { - return fmt.Errorf("ARGS: invalid key %q", keyString) + unknownArgs = append(unknownArgs, pair) + continue } + u := keyField.Addr().Interface().(encoding.TextUnmarshaler) err := u.UnmarshalText([]byte(valueString)) if err != nil { return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err) } } + + isIgnoreUnknown := GetKeyField("IgnoreUnknown", containerValue).Bool() + if len(unknownArgs) > 0 && !isIgnoreUnknown { + return fmt.Errorf("ARGS: unknown args %q", unknownArgs) + } return nil } From dc44feb5b57340d5934c3b5f8b155b74fb0a9133 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 17 Mar 2016 14:09:54 +0100 Subject: [PATCH 029/134] pkg/types: add tests for args --- types/args.go | 4 +- types/args_test.go | 92 +++++++++++++++++++++++++++++++++++++++ types/types_suite_test.go | 13 ++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 types/args_test.go create mode 100644 types/types_suite_test.go diff --git a/types/args.go b/types/args.go index c4e3c55b..3b667b0f 100644 --- a/types/args.go +++ b/types/args.go @@ -47,9 +47,9 @@ type CommonArgs struct { IgnoreUnknown UnmarshallableBool `json:"ignoreunknown,omitempty"` } -// getKeyField is a helper function to receive Values +// GetKeyField is a helper function to receive Values // Values that represent a pointer to a struct -func getKeyField(keyString string, v reflect.Value) reflect.Value { +func GetKeyField(keyString string, v reflect.Value) reflect.Value { return v.Elem().FieldByName(keyString) } diff --git a/types/args_test.go b/types/args_test.go new file mode 100644 index 00000000..123548cc --- /dev/null +++ b/types/args_test.go @@ -0,0 +1,92 @@ +package types_test + +import ( + "reflect" + + . "github.com/appc/cni/pkg/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("UnmarshallableBool UnmarshalText", func() { + DescribeTable("string to bool detection should succeed in all cases", + func(inputs []string, expected bool) { + for _, s := range inputs { + var ub UnmarshallableBool + err := ub.UnmarshalText([]byte(s)) + Expect(err).ToNot(HaveOccurred()) + Expect(ub).To(Equal(UnmarshallableBool(expected))) + } + }, + Entry("parse to true", []string{"True", "true", "1"}, true), + Entry("parse to false", []string{"False", "false", "0"}, false), + ) + + Context("When passed an invalid value", func() { + It("should result in an error", func() { + var ub UnmarshallableBool + err := ub.UnmarshalText([]byte("invalid")) + Expect(err).To(HaveOccurred()) + }) + }) +}) + +var _ = Describe("GetKeyField", func() { + type testcontainer struct { + Valid string `json:"valid,omitempty"` + } + var ( + container = testcontainer{Valid: "valid"} + containerInterface = func(i interface{}) interface{} { return i }(&container) + containerValue = reflect.ValueOf(containerInterface) + ) + Context("When a valid field is provided", func() { + It("should return the correct field", func() { + field := GetKeyField("Valid", containerValue) + Expect(field.String()).To(Equal("valid")) + }) + }) +}) + +var _ = Describe("LoadArgs", func() { + Context("When no arguments are passed", func() { + It("LoadArgs should succeed", func() { + err := LoadArgs("", struct{}{}) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When unknown arguments are passed and ignored", func() { + It("LoadArgs should succeed", func() { + ca := CommonArgs{} + err := LoadArgs("IgnoreUnknown=True;Unk=nown", &ca) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("When unknown arguments are passed and not ignored", func() { + It("LoadArgs should fail", func() { + ca := CommonArgs{} + err := LoadArgs("Unk=nown", &ca) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("When unknown arguments are passed and explicitly not ignored", func() { + It("LoadArgs should fail", func() { + ca := CommonArgs{} + err := LoadArgs("IgnoreUnknown=0, Unk=nown", &ca) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("When known arguments are passed", func() { + It("LoadArgs should succeed", func() { + ca := CommonArgs{} + err := LoadArgs("IgnoreUnknown=1", &ca) + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) diff --git a/types/types_suite_test.go b/types/types_suite_test.go new file mode 100644 index 00000000..b026169c --- /dev/null +++ b/types/types_suite_test.go @@ -0,0 +1,13 @@ +package types_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Types Suite") +} From b87cf1ba9c0e51552300c8149e41f89e9d563dc0 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 22 Mar 2016 17:16:59 -0700 Subject: [PATCH 030/134] pkg: add a function to generate chain names Adds a function to generate chain names for use in iptables and ports all drivers to use that function. Also adds tests for the said function. --- utils/utils.go | 20 ++++++++++++++++++++ utils/utils_suite_test.go | 13 +++++++++++++ utils/utils_test.go | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 utils/utils.go create mode 100644 utils/utils_suite_test.go create mode 100644 utils/utils_test.go diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 00000000..eaf48d0b --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,20 @@ +package utils + +import ( + "crypto/sha512" + "fmt" +) + +// 29 - len('CNI') - 2*len('-') +const maxNameLen = 16 + +// Generates a chain name to be used with iptables. +// Ensures that the generated name is less than +// 29 chars in length +func FormatChainName(name string, id string) string { + h := sha512.Sum512([]byte(id)) + if len(name) > maxNameLen { + return fmt.Sprintf("CNI-%s-%x", name[:len(name)-maxNameLen], h[:8]) + } + return fmt.Sprintf("CNI-%s-%x", name, h[:8]) +} diff --git a/utils/utils_suite_test.go b/utils/utils_suite_test.go new file mode 100644 index 00000000..f160db60 --- /dev/null +++ b/utils/utils_suite_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 00000000..e9b9f9bf --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,18 @@ +package utils + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Utils", func() { + It("should format a short name", func() { + chain := FormatChainName("test", "1234") + Expect(chain).To(Equal("CNI-test-d404559f602eab6f")) + }) + + It("should truncate a long name", func() { + chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + Expect(chain).To(Equal("CNI-testalongnamethat-d404559f602eab6f")) + }) +}) From b4a2a1fa5176c868480a133fc83d805a8ae577c8 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 30 Mar 2016 19:17:37 +0200 Subject: [PATCH 031/134] pkg/utils: use name+id for hash and extend tests --- utils/utils.go | 11 +++++------ utils/utils_test.go | 29 +++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index eaf48d0b..a07ccfab 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,16 +5,15 @@ import ( "fmt" ) -// 29 - len('CNI') - 2*len('-') -const maxNameLen = 16 +const MaxChainLength = 29 - len("CNI-") // Generates a chain name to be used with iptables. // Ensures that the generated name is less than // 29 chars in length func FormatChainName(name string, id string) string { - h := sha512.Sum512([]byte(id)) - if len(name) > maxNameLen { - return fmt.Sprintf("CNI-%s-%x", name[:len(name)-maxNameLen], h[:8]) + chain := fmt.Sprintf("%x", sha512.Sum512([]byte(name+id))) + if len(chain) > MaxChainLength { + chain = chain[:MaxChainLength] } - return fmt.Sprintf("CNI-%s-%x", name, h[:8]) + return fmt.Sprintf("CNI-%s", chain) } diff --git a/utils/utils_test.go b/utils/utils_test.go index e9b9f9bf..05fde8a9 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -6,13 +6,34 @@ import ( ) var _ = Describe("Utils", func() { - It("should format a short name", func() { + It("must format a short name", func() { chain := FormatChainName("test", "1234") - Expect(chain).To(Equal("CNI-test-d404559f602eab6f")) + Expect(len(chain) == 29).To(Equal(true)) + Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8b")) }) - It("should truncate a long name", func() { + It("must truncate a long name", func() { chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234") - Expect(chain).To(Equal("CNI-testalongnamethat-d404559f602eab6f")) + Expect(len(chain) == 29).To(Equal(true)) + Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) }) + + It("must be predictable", func() { + chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + Expect(len(chain1) == 29).To(Equal(true)) + Expect(len(chain2) == 29).To(Equal(true)) + Expect(chain1).To(Equal(chain2)) + }) + + It("must change when a character changes", func() { + chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") + chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235") + Expect(len(chain1) == 29).To(Equal(true)) + Expect(len(chain2) == 29).To(Equal(true)) + Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) + Expect(chain2).NotTo(Equal("CNI-374f33fe84ab0ed84dcdebe38")) + Expect(chain1).NotTo(Equal(chain2)) + }) + }) From f96a71615454caf2c1f6a1792d7064143b443c0b Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 30 Mar 2016 19:40:20 +0200 Subject: [PATCH 032/134] pkg/utils: use constant for chain prefix --- utils/utils.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index a07ccfab..e1468aaf 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,7 +5,8 @@ import ( "fmt" ) -const MaxChainLength = 29 - len("CNI-") +const ChainCNIPrefix = ("CNI-") +const MaxChainLength = 29 - len(ChainCNIPrefix) // Generates a chain name to be used with iptables. // Ensures that the generated name is less than @@ -15,5 +16,5 @@ func FormatChainName(name string, id string) string { if len(chain) > MaxChainLength { chain = chain[:MaxChainLength] } - return fmt.Sprintf("CNI-%s", chain) + return fmt.Sprintf("%s%s", ChainCNIPrefix, chain) } From f429750105e01ee05c027418fb631839da222720 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 30 Mar 2016 19:40:31 +0200 Subject: [PATCH 033/134] pkg/utils: fix docstring --- utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/utils.go b/utils/utils.go index e1468aaf..1566252d 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -9,7 +9,7 @@ const ChainCNIPrefix = ("CNI-") const MaxChainLength = 29 - len(ChainCNIPrefix) // Generates a chain name to be used with iptables. -// Ensures that the generated name is less than +// Ensures that the generated chain name is less than // 29 chars in length func FormatChainName(name string, id string) string { chain := fmt.Sprintf("%x", sha512.Sum512([]byte(name+id))) From d4e088aad40ff0f03016aa25feff41c5b0913f31 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 31 Mar 2016 10:08:52 +0200 Subject: [PATCH 034/134] pkg/utils: split and unexport constants --- utils/utils.go | 17 +++++++++-------- utils/utils_test.go | 13 ++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/utils/utils.go b/utils/utils.go index 1566252d..5877fb62 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -5,16 +5,17 @@ import ( "fmt" ) -const ChainCNIPrefix = ("CNI-") -const MaxChainLength = 29 - len(ChainCNIPrefix) +const ( + maxChainLength = 28 + chainPrefix = "CNI-" + prefixLength = len(chainPrefix) +) // Generates a chain name to be used with iptables. // Ensures that the generated chain name is less than -// 29 chars in length +// maxChainLength chars in length func FormatChainName(name string, id string) string { - chain := fmt.Sprintf("%x", sha512.Sum512([]byte(name+id))) - if len(chain) > MaxChainLength { - chain = chain[:MaxChainLength] - } - return fmt.Sprintf("%s%s", ChainCNIPrefix, chain) + chainBytes := sha512.Sum512([]byte(name + id)) + chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes) + return chain[:maxChainLength] } diff --git a/utils/utils_test.go b/utils/utils_test.go index 05fde8a9..00e3b4f4 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -8,32 +8,31 @@ import ( var _ = Describe("Utils", func() { It("must format a short name", func() { chain := FormatChainName("test", "1234") - Expect(len(chain) == 29).To(Equal(true)) + Expect(len(chain)).To(Equal(maxChainLength)) Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8b")) }) It("must truncate a long name", func() { chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234") - Expect(len(chain) == 29).To(Equal(true)) + Expect(len(chain)).To(Equal(maxChainLength)) Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) }) It("must be predictable", func() { chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") - Expect(len(chain1) == 29).To(Equal(true)) - Expect(len(chain2) == 29).To(Equal(true)) + Expect(len(chain1)).To(Equal(maxChainLength)) + Expect(len(chain2)).To(Equal(maxChainLength)) Expect(chain1).To(Equal(chain2)) }) It("must change when a character changes", func() { chain1 := FormatChainName("testalongnamethatdoesnotmakesense", "1234") chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235") - Expect(len(chain1) == 29).To(Equal(true)) - Expect(len(chain2) == 29).To(Equal(true)) + Expect(len(chain1)).To(Equal(maxChainLength)) + Expect(len(chain2)).To(Equal(maxChainLength)) Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) Expect(chain2).NotTo(Equal("CNI-374f33fe84ab0ed84dcdebe38")) Expect(chain1).NotTo(Equal(chain2)) }) - }) From 9612d36615bb21d9d48f189e68931e3bfddf2704 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 31 Mar 2016 11:50:18 +0200 Subject: [PATCH 035/134] pkg/utils: fix docstring --- utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/utils.go b/utils/utils.go index 5877fb62..ea29c965 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -12,7 +12,7 @@ const ( ) // Generates a chain name to be used with iptables. -// Ensures that the generated chain name is less than +// Ensures that the generated chain name is exactly // maxChainLength chars in length func FormatChainName(name string, id string) string { chainBytes := sha512.Sum512([]byte(name + id)) From 42b74b9eb59321db700f01a0e1154bc2acb8768a Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 31 Mar 2016 11:55:46 +0200 Subject: [PATCH 036/134] pkg/utils: remove unneeded condition in tests --- utils/utils_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/utils_test.go b/utils/utils_test.go index 00e3b4f4..20a9fcd5 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -32,7 +32,6 @@ var _ = Describe("Utils", func() { Expect(len(chain1)).To(Equal(maxChainLength)) Expect(len(chain2)).To(Equal(maxChainLength)) Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) - Expect(chain2).NotTo(Equal("CNI-374f33fe84ab0ed84dcdebe38")) Expect(chain1).NotTo(Equal(chain2)) }) }) From 6c9b5a361a0c76bda46f8d9e41713711ffe8a899 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 31 Mar 2016 15:44:54 +0200 Subject: [PATCH 037/134] *: add comment to iptables rules for ipmasq --- ip/ipmasq.go | 12 ++++++------ utils/utils.go | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ip/ipmasq.go b/ip/ipmasq.go index 6901f69e..8ee27971 100644 --- a/ip/ipmasq.go +++ b/ip/ipmasq.go @@ -23,7 +23,7 @@ import ( // SetupIPMasq installs iptables rules to masquerade traffic // coming from ipn and going outside of it -func SetupIPMasq(ipn *net.IPNet, chain string) error { +func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error { ipt, err := iptables.New() if err != nil { return fmt.Errorf("failed to locate iptables: %v", err) @@ -36,25 +36,25 @@ func SetupIPMasq(ipn *net.IPNet, chain string) error { } } - if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT"); err != nil { + if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil { return err } - if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE"); err != nil { + if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil { return err } - return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain) + return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment) } // TeardownIPMasq undoes the effects of SetupIPMasq -func TeardownIPMasq(ipn *net.IPNet, chain string) error { +func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error { ipt, err := iptables.New() if err != nil { return fmt.Errorf("failed to locate iptables: %v", err) } - if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain); err != nil { + if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment); err != nil { return err } diff --git a/utils/utils.go b/utils/utils.go index ea29c965..7ec139fd 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -19,3 +19,9 @@ func FormatChainName(name string, id string) string { chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes) return chain[:maxChainLength] } + +// FormatComment returns a comment used for easier +// rule identification within iptables. +func FormatComment(name string, id string) string { + return fmt.Sprintf("name: %q id: %q", name, id) +} From 2c1633f8e9529f29dbdee404b801cafdd2864383 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Thu, 31 Mar 2016 17:11:11 +0200 Subject: [PATCH 038/134] pkg/utils: correct the test's expected chain names --- utils/utils_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/utils_test.go b/utils/utils_test.go index 20a9fcd5..91bb8654 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -9,13 +9,13 @@ var _ = Describe("Utils", func() { It("must format a short name", func() { chain := FormatChainName("test", "1234") Expect(len(chain)).To(Equal(maxChainLength)) - Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8b")) + Expect(chain).To(Equal("CNI-2bbe0c48b91a7d1b8a6753a8")) }) It("must truncate a long name", func() { chain := FormatChainName("testalongnamethatdoesnotmakesense", "1234") Expect(len(chain)).To(Equal(maxChainLength)) - Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) + Expect(chain).To(Equal("CNI-374f33fe84ab0ed84dcdebe3")) }) It("must be predictable", func() { @@ -31,7 +31,7 @@ var _ = Describe("Utils", func() { chain2 := FormatChainName("testalongnamethatdoesnotmakesense", "1235") Expect(len(chain1)).To(Equal(maxChainLength)) Expect(len(chain2)).To(Equal(maxChainLength)) - Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe38")) + Expect(chain1).To(Equal("CNI-374f33fe84ab0ed84dcdebe3")) Expect(chain1).NotTo(Equal(chain2)) }) }) From 11c0bfada8e8b82221ad7608749423ece981b6f5 Mon Sep 17 00:00:00 2001 From: Jonathan Boulle Date: Fri, 1 Apr 2016 15:35:21 +0200 Subject: [PATCH 039/134] *: add missing license headers + check --- invoke/delegate.go | 14 ++++++++++++++ invoke/find_test.go | 14 ++++++++++++++ invoke/invoke_suite_test.go | 14 ++++++++++++++ ns/ns_suite_test.go | 14 ++++++++++++++ ns/ns_test.go | 14 ++++++++++++++ skel/skel_suite_test.go | 14 ++++++++++++++ skel/skel_test.go | 14 ++++++++++++++ types/args_test.go | 14 ++++++++++++++ types/types_suite_test.go | 14 ++++++++++++++ utils/utils.go | 14 ++++++++++++++ utils/utils_suite_test.go | 14 ++++++++++++++ utils/utils_test.go | 14 ++++++++++++++ 12 files changed, 168 insertions(+) diff --git a/invoke/delegate.go b/invoke/delegate.go index 9864e6f5..0a8198c3 100644 --- a/invoke/delegate.go +++ b/invoke/delegate.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package invoke import ( diff --git a/invoke/find_test.go b/invoke/find_test.go index 00baa313..4135538d 100644 --- a/invoke/find_test.go +++ b/invoke/find_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package invoke_test import ( diff --git a/invoke/invoke_suite_test.go b/invoke/invoke_suite_test.go index e570c964..72eb837f 100644 --- a/invoke/invoke_suite_test.go +++ b/invoke/invoke_suite_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package invoke_test import ( diff --git a/ns/ns_suite_test.go b/ns/ns_suite_test.go index ff26ec2e..e2adaa4e 100644 --- a/ns/ns_suite_test.go +++ b/ns/ns_suite_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ns_test import ( diff --git a/ns/ns_test.go b/ns/ns_test.go index ad52f7f4..d9f182cf 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package ns_test import ( diff --git a/skel/skel_suite_test.go b/skel/skel_suite_test.go index 4820d5d9..df5a8e77 100644 --- a/skel/skel_suite_test.go +++ b/skel/skel_suite_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package skel import ( diff --git a/skel/skel_test.go b/skel/skel_test.go index c6fb4e80..44695648 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package skel import ( diff --git a/types/args_test.go b/types/args_test.go index 123548cc..23aaf817 100644 --- a/types/args_test.go +++ b/types/args_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package types_test import ( diff --git a/types/types_suite_test.go b/types/types_suite_test.go index b026169c..2b178cee 100644 --- a/types/types_suite_test.go +++ b/types/types_suite_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package types_test import ( diff --git a/utils/utils.go b/utils/utils.go index 7ec139fd..33a2aa79 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package utils import ( diff --git a/utils/utils_suite_test.go b/utils/utils_suite_test.go index f160db60..ee614a70 100644 --- a/utils/utils_suite_test.go +++ b/utils/utils_suite_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package utils_test import ( diff --git a/utils/utils_test.go b/utils/utils_test.go index 91bb8654..d703de42 100644 --- a/utils/utils_test.go +++ b/utils/utils_test.go @@ -1,3 +1,17 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package utils import ( From e1bc41a42a239f2ac77e9940f5e688ba8b6c69c5 Mon Sep 17 00:00:00 2001 From: Piotr Skamruk Date: Mon, 7 Mar 2016 16:40:27 +0100 Subject: [PATCH 040/134] pkg/utils: add functions to work with sysctl --- utils/sysctl/sysctl_linux.go | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 utils/sysctl/sysctl_linux.go diff --git a/utils/sysctl/sysctl_linux.go b/utils/sysctl/sysctl_linux.go new file mode 100644 index 00000000..c0fba382 --- /dev/null +++ b/utils/sysctl/sysctl_linux.go @@ -0,0 +1,58 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// build +linux + +package sysctl + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" +) + +// Sysctl provides a method to set/get values from /proc/sys - in linux systems +// new interface to set/get values of variables formerly handled by sysctl syscall +// If optional `params` have only one string value - this function will +// set this value into coresponding sysctl variable +func Sysctl(name string, params ...string) (string, error) { + if len(params) > 1 { + return "", fmt.Errorf("unexcepted additional parameters") + } else if len(params) == 1 { + return setSysctl(name, params[0]) + } + return getSysctl(name) +} + +func getSysctl(name string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + data, err := ioutil.ReadFile(fullName) + if err != nil { + return "", err + } + + return string(data[:len(data)-1]), nil +} + +func setSysctl(name, value string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil { + return "", err + } + + return getSysctl(name) +} From 4a79ac4cda42489cff0dde37156f15bf34c3093c Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:28:10 -0700 Subject: [PATCH 041/134] Extract testhelpers from loopback test suite --- testhelpers/testhelpers.go | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 testhelpers/testhelpers.go diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go new file mode 100644 index 00000000..f1ccb24e --- /dev/null +++ b/testhelpers/testhelpers.go @@ -0,0 +1,69 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package testhelpers + +import ( + "fmt" + "os" + "runtime" + + "golang.org/x/sys/unix" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func MakeNetworkNS(containerID string) string { + namespace := "/var/run/netns/" + containerID + pid := unix.Getpid() + tid := unix.Gettid() + + err := os.MkdirAll("/var/run/netns", 0600) + Expect(err).NotTo(HaveOccurred()) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + go (func() { + defer GinkgoRecover() + + err = unix.Unshare(unix.CLONE_NEWNET) + Expect(err).NotTo(HaveOccurred()) + + fd, err := os.Create(namespace) + Expect(err).NotTo(HaveOccurred()) + defer fd.Close() + + err = unix.Mount("/proc/self/ns/net", namespace, "none", unix.MS_BIND, "") + Expect(err).NotTo(HaveOccurred()) + })() + + Eventually(namespace).Should(BeAnExistingFile()) + + fd, err := unix.Open(fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid), unix.O_RDONLY, 0) + Expect(err).NotTo(HaveOccurred()) + + defer unix.Close(fd) + + _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(fd), uintptr(unix.CLONE_NEWNET), 0) + Expect(e1).To(BeZero()) + + return namespace +} + +func RemoveNetworkNS(networkNS string) error { + err := unix.Unmount(networkNS, unix.MNT_DETACH) + + err = os.RemoveAll(networkNS) + return err +} From 5f757b6af7677a06ef976d8d37d7f8d316cf00f3 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:35:49 -0700 Subject: [PATCH 042/134] Extract inode inspection functions into testhelpers --- ns/ns_test.go | 38 +++++++++++--------------------------- testhelpers/testhelpers.go | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ns/ns_test.go b/ns/ns_test.go index d9f182cf..42fc6322 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -22,28 +22,12 @@ import ( "os/exec" "path/filepath" - "golang.org/x/sys/unix" - "github.com/appc/cni/pkg/ns" + "github.com/appc/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -func getInode(path string) (uint64, error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer file.Close() - return getInodeF(file) -} - -func getInodeF(file *os.File) (uint64, error) { - stat := &unix.Stat_t{} - err := unix.Fstat(int(file.Fd()), stat) - return stat.Ino, err -} - const CurrentNetNS = "/proc/self/ns/net" var _ = Describe("Linux namespace operations", func() { @@ -81,13 +65,13 @@ var _ = Describe("Linux namespace operations", func() { }) It("executes the callback within the target network namespace", func() { - expectedInode, err := getInode(targetNetNSPath) + expectedInode, err := testhelpers.GetInode(targetNetNSPath) Expect(err).NotTo(HaveOccurred()) var actualInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - actualInode, innerErr = getInode(CurrentNetNS) + actualInode, innerErr = testhelpers.GetInode(CurrentNetNS) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -97,13 +81,13 @@ var _ = Describe("Linux namespace operations", func() { }) It("provides the original namespace as the argument to the callback", func() { - hostNSInode, err := getInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) var inputNSInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(inputNS *os.File) error { - inputNSInode, err = getInodeF(inputNS) + inputNSInode, err = testhelpers.GetInodeF(inputNS) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -113,7 +97,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("restores the calling thread to the original network namespace", func() { - preTestInode, err := getInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { @@ -121,7 +105,7 @@ var _ = Describe("Linux namespace operations", func() { }) Expect(err).NotTo(HaveOccurred()) - postTestInode, err := getInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -129,14 +113,14 @@ var _ = Describe("Linux namespace operations", func() { Context("when the callback returns an error", func() { It("restores the calling thread to the original namespace before returning", func() { - preTestInode, err := getInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { return errors.New("potato") }) - postTestInode, err := getInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -152,10 +136,10 @@ var _ = Describe("Linux namespace operations", func() { Describe("validating inode mapping to namespaces", func() { It("checks that different namespaces have different inodes", func() { - hostNSInode, err := getInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) - testNsInode, err := getInode(targetNetNSPath) + testNsInode, err := testhelpers.GetInode(targetNetNSPath) Expect(err).NotTo(HaveOccurred()) Expect(hostNSInode).NotTo(Equal(0)) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index f1ccb24e..4eb42bdc 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -11,6 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +// Package testhelpers provides common support behavior for tests package testhelpers import ( @@ -24,6 +26,21 @@ import ( . "github.com/onsi/gomega" ) +func GetInode(path string) (uint64, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + return GetInodeF(file) +} + +func GetInodeF(file *os.File) (uint64, error) { + stat := &unix.Stat_t{} + err := unix.Fstat(int(file.Fd()), stat) + return stat.Ino, err +} + func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID pid := unix.Getpid() From 82851a860e80bd60e2240fecf765aa6107c0e651 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:48:50 -0700 Subject: [PATCH 043/134] Add basic unit tests of testhelpers --- testhelpers/testhelpers_suite_test.go | 31 +++++++++ testhelpers/testhelpers_test.go | 96 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 testhelpers/testhelpers_suite_test.go create mode 100644 testhelpers/testhelpers_test.go diff --git a/testhelpers/testhelpers_suite_test.go b/testhelpers/testhelpers_suite_test.go new file mode 100644 index 00000000..88bfc3d6 --- /dev/null +++ b/testhelpers/testhelpers_suite_test.go @@ -0,0 +1,31 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testhelpers_test + +import ( + "math/rand" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTesthelpers(t *testing.T) { + rand.Seed(config.GinkgoConfig.RandomSeed) + RegisterFailHandler(Fail) + RunSpecs(t, "Testhelpers Suite") +} diff --git a/testhelpers/testhelpers_test.go b/testhelpers/testhelpers_test.go new file mode 100644 index 00000000..ce328f01 --- /dev/null +++ b/testhelpers/testhelpers_test.go @@ -0,0 +1,96 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testhelpers_test contains unit tests of the testhelpers +// +// Some of this stuff is non-trivial and can interact in surprising ways +// with the Go runtime. Better be safe. +package testhelpers_test + +import ( + "fmt" + "math/rand" + "path/filepath" + + "golang.org/x/sys/unix" + + "github.com/appc/cni/pkg/testhelpers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test helper functions", func() { + Describe("MakeNetworkNS", func() { + It("should return the filepath to a network namespace", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + nsPath := testhelpers.MakeNetworkNS(containerID) + + Expect(nsPath).To(BeAnExistingFile()) + + testhelpers.RemoveNetworkNS(containerID) + }) + + It("should return a network namespace different from that of the caller", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + + By("discovering the inode of the current netns") + originalNetNSPath := currentNetNSPath() + originalNetNSInode, err := testhelpers.GetInode(originalNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("creating a new netns") + createdNetNSPath := testhelpers.MakeNetworkNS(containerID) + defer testhelpers.RemoveNetworkNS(createdNetNSPath) + + By("discovering the inode of the created netns") + createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("comparing the inodes") + Expect(createdNetNSInode).NotTo(Equal(originalNetNSInode)) + }) + + It("should not leak the new netns onto any threads in the process", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + + By("creating a new netns") + createdNetNSPath := testhelpers.MakeNetworkNS(containerID) + defer testhelpers.RemoveNetworkNS(createdNetNSPath) + + By("discovering the inode of the created netns") + createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("comparing against the netns inode of every thread in the process") + for _, netnsPath := range allNetNSInCurrentProcess() { + netnsInode, err := testhelpers.GetInode(netnsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(netnsInode).NotTo(Equal(createdNetNSInode)) + } + }) + }) +}) + +func currentNetNSPath() string { + pid := unix.Getpid() + tid := unix.Gettid() + return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) +} + +func allNetNSInCurrentProcess() []string { + pid := unix.Getpid() + paths, err := filepath.Glob(fmt.Sprintf("/proc/%d/task/*/ns/net", pid)) + Expect(err).NotTo(HaveOccurred()) + return paths +} From c085ec98fdb537c8d6b7a62e7073853582d096df Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 19:44:00 -0700 Subject: [PATCH 044/134] Fix issues with MakeNetworkNS test helper --- testhelpers/testhelpers.go | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 4eb42bdc..0963121d 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "runtime" + "sync" "golang.org/x/sys/unix" @@ -43,37 +44,51 @@ func GetInodeF(file *os.File) (uint64, error) { func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID - pid := unix.Getpid() - tid := unix.Gettid() err := os.MkdirAll("/var/run/netns", 0600) Expect(err).NotTo(HaveOccurred()) - runtime.LockOSThread() - defer runtime.UnlockOSThread() + // create an empty file at the mount point + mountPointFd, err := os.Create(namespace) + Expect(err).NotTo(HaveOccurred()) + mountPointFd.Close() + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function go (func() { + defer wg.Done() + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer GinkgoRecover() + // capture current thread's original netns + pid := unix.Getpid() + tid := unix.Gettid() + currentThreadNetNSPath := fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) + originalNetNS, err := unix.Open(currentThreadNetNSPath, unix.O_RDONLY, 0) + Expect(err).NotTo(HaveOccurred()) + defer unix.Close(originalNetNS) + + // create a new netns on the current thread err = unix.Unshare(unix.CLONE_NEWNET) Expect(err).NotTo(HaveOccurred()) - fd, err := os.Create(namespace) + // bind mount the new netns from the current thread onto the mount point + err = unix.Mount(currentThreadNetNSPath, namespace, "none", unix.MS_BIND, "") Expect(err).NotTo(HaveOccurred()) - defer fd.Close() - err = unix.Mount("/proc/self/ns/net", namespace, "none", unix.MS_BIND, "") - Expect(err).NotTo(HaveOccurred()) + // reset current thread's netns to the original + _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(originalNetNS), uintptr(unix.CLONE_NEWNET), 0) + Expect(e1).To(BeZero()) })() - Eventually(namespace).Should(BeAnExistingFile()) - - fd, err := unix.Open(fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid), unix.O_RDONLY, 0) - Expect(err).NotTo(HaveOccurred()) - - defer unix.Close(fd) - - _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(fd), uintptr(unix.CLONE_NEWNET), 0) - Expect(e1).To(BeZero()) + wg.Wait() return namespace } From d701ca6c824bb22f84714be93ba3c1553a14a90f Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 20:27:02 -0700 Subject: [PATCH 045/134] Document use of goroutine and lockosthread in test helpers --- testhelpers/testhelpers.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 0963121d..5ceb3389 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -42,6 +42,33 @@ func GetInodeF(file *os.File) (uint64, error) { return stat.Ino, err } +/* +A note about goroutines, Linux namespaces and runtime.LockOSThread + +In Linux, network namespaces have thread affinity. + +In the Go language runtime, goroutines do not have affinity for OS threads. +The Go runtime scheduler moves goroutines around amongst OS threads. It +is supposed to be transparent to the Go programmer. + +In order to address cases where the programmer needs thread affinity, Go +provides runtime.LockOSThread and runtime.UnlockOSThread() + +However, the Go runtime does not reference count the Lock and Unlock calls. +Repeated calls to Lock will succeed, but the first call to Unlock will unlock +everything. Therefore, it is dangerous to hide a Lock/Unlock in a library +function, such as in this package. + +The code below, in MakeNetworkNS, avoids this problem by spinning up a new +Go routine specifically so that LockOSThread can be called on it. Thus +goroutine-thread affinity is maintained long enough to perform all the required +namespace operations. + +Because the LockOSThread call is performed inside this short-lived goroutine, +there is no effect either way on the caller's goroutine-thread affinity. + +* */ + func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID @@ -58,7 +85,7 @@ func MakeNetworkNS(containerID string) string { // do namespace work in a dedicated goroutine, so that we can safely // Lock/Unlock OSThread without upsetting the lock/unlock state of - // the caller of this function + // the caller of this function. See block comment above. go (func() { defer wg.Done() From c272c49555be0dce320288aa88cd11095598b89c Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 6 Apr 2016 11:03:31 -0500 Subject: [PATCH 046/134] ns: fix reading net namespace in multi-threaded processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /proc/self/ns/net gives the main thread's namespace, not necessarily the namespace of the thread that's running the testcases. This causes sporadic failures of the tests. For example, with a testcase reading inodes after switching netns: /proc/27686/task/27689/ns/net 4026532565 /proc/self/ns/net 4026531969 /proc/27686/task/27689/ns/net 4026532565 See also: https://github.com/vishvananda/netns/commit/008d17ae001344769b031375bdb38a86219154c6 Running Suite: pkg/ns Suite =========================== Random Seed: 1459953577 Will run 6 of 6 specs • Failure [0.028 seconds] Linux namespace operations /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:167 WithNetNS /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:166 executes the callback within the target network namespace [It] /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:97 Expected : 4026531969 to equal : 4026532565 /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96 ------------------------------ ••••• Summarizing 1 Failure: [Fail] Linux namespace operations WithNetNS [It] executes the callback within the target network namespace /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96 Ran 6 of 6 Specs in 0.564 seconds FAIL! -- 5 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestNs (0.56s) FAIL --- ns/ns_test.go | 22 +++++++--------------- testhelpers/testhelpers.go | 14 +++++++++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ns/ns_test.go b/ns/ns_test.go index 42fc6322..7ad882f5 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -28,13 +28,9 @@ import ( . "github.com/onsi/gomega" ) -const CurrentNetNS = "/proc/self/ns/net" - var _ = Describe("Linux namespace operations", func() { Describe("WithNetNS", func() { var ( - originalNetNS *os.File - targetNetNSName string targetNetNSPath string targetNetNS *os.File @@ -42,8 +38,6 @@ var _ = Describe("Linux namespace operations", func() { BeforeEach(func() { var err error - originalNetNS, err = os.Open(CurrentNetNS) - Expect(err).NotTo(HaveOccurred()) targetNetNSName = fmt.Sprintf("test-netns-%d", rand.Int()) @@ -60,8 +54,6 @@ var _ = Describe("Linux namespace operations", func() { err := exec.Command("ip", "netns", "del", targetNetNSName).Run() Expect(err).NotTo(HaveOccurred()) - - Expect(originalNetNS.Close()).To(Succeed()) }) It("executes the callback within the target network namespace", func() { @@ -71,7 +63,7 @@ var _ = Describe("Linux namespace operations", func() { var actualInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - actualInode, innerErr = testhelpers.GetInode(CurrentNetNS) + actualInode, innerErr = testhelpers.GetInodeCurNetNS() return nil }) Expect(err).NotTo(HaveOccurred()) @@ -81,7 +73,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("provides the original namespace as the argument to the callback", func() { - hostNSInode, err := testhelpers.GetInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) var inputNSInode uint64 @@ -97,7 +89,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("restores the calling thread to the original network namespace", func() { - preTestInode, err := testhelpers.GetInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { @@ -105,7 +97,7 @@ var _ = Describe("Linux namespace operations", func() { }) Expect(err).NotTo(HaveOccurred()) - postTestInode, err := testhelpers.GetInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -113,14 +105,14 @@ var _ = Describe("Linux namespace operations", func() { Context("when the callback returns an error", func() { It("restores the calling thread to the original namespace before returning", func() { - preTestInode, err := testhelpers.GetInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { return errors.New("potato") }) - postTestInode, err := testhelpers.GetInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -136,7 +128,7 @@ var _ = Describe("Linux namespace operations", func() { Describe("validating inode mapping to namespaces", func() { It("checks that different namespaces have different inodes", func() { - hostNSInode, err := testhelpers.GetInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) testNsInode, err := testhelpers.GetInode(targetNetNSPath) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 0963121d..004006a9 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -27,6 +27,16 @@ import ( . "github.com/onsi/gomega" ) +func getCurrentThreadNetNSPath() string { + pid := unix.Getpid() + tid := unix.Gettid() + return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) +} + +func GetInodeCurNetNS() (uint64, error) { + return GetInode(getCurrentThreadNetNSPath()) +} + func GetInode(path string) (uint64, error) { file, err := os.Open(path) if err != nil { @@ -68,9 +78,7 @@ func MakeNetworkNS(containerID string) string { defer GinkgoRecover() // capture current thread's original netns - pid := unix.Getpid() - tid := unix.Gettid() - currentThreadNetNSPath := fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) + currentThreadNetNSPath := getCurrentThreadNetNSPath() originalNetNS, err := unix.Open(currentThreadNetNSPath, unix.O_RDONLY, 0) Expect(err).NotTo(HaveOccurred()) defer unix.Close(originalNetNS) From 9079565e0d28cdb5e5bd01fd80725ee8c94272da Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 4 May 2016 16:06:05 +0200 Subject: [PATCH 047/134] *: appc/cni -> containernetworking/cni The project has been moved so internally we simply rename everything. Consumers are recommended to update their vendored version of cni. --- invoke/delegate.go | 2 +- invoke/exec.go | 2 +- invoke/find_test.go | 2 +- ip/link.go | 2 +- ipam/ipam.go | 6 +++--- ns/ns_test.go | 4 ++-- skel/skel.go | 2 +- testhelpers/testhelpers_test.go | 2 +- types/args_test.go | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/invoke/delegate.go b/invoke/delegate.go index 0a8198c3..ddf1d172 100644 --- a/invoke/delegate.go +++ b/invoke/delegate.go @@ -19,7 +19,7 @@ import ( "os" "strings" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) func DelegateAdd(delegatePlugin string, netconf []byte) (*types.Result, error) { diff --git a/invoke/exec.go b/invoke/exec.go index 337bfcb8..a85eede6 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -21,7 +21,7 @@ import ( "os" "os/exec" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) func pluginErr(err error, output []byte) error { diff --git a/invoke/find_test.go b/invoke/find_test.go index 4135538d..be4cc2dd 100644 --- a/invoke/find_test.go +++ b/invoke/find_test.go @@ -19,7 +19,7 @@ import ( "io/ioutil" "path/filepath" - "github.com/appc/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/invoke" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/ip/link.go b/ip/link.go index 1ba529da..df168124 100644 --- a/ip/link.go +++ b/ip/link.go @@ -20,7 +20,7 @@ import ( "net" "os" - "github.com/appc/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/ns" "github.com/vishvananda/netlink" ) diff --git a/ipam/ipam.go b/ipam/ipam.go index f0adfb7f..d9fbff74 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -18,9 +18,9 @@ import ( "fmt" "os" - "github.com/appc/cni/pkg/invoke" - "github.com/appc/cni/pkg/ip" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/types" "github.com/vishvananda/netlink" ) diff --git a/ns/ns_test.go b/ns/ns_test.go index 7ad882f5..a901eb35 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -22,8 +22,8 @@ import ( "os/exec" "path/filepath" - "github.com/appc/cni/pkg/ns" - "github.com/appc/cni/pkg/testhelpers" + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/skel/skel.go b/skel/skel.go index 5c3532db..7347b078 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -22,7 +22,7 @@ import ( "log" "os" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) // CmdArgs captures all the arguments passed in to the plugin diff --git a/testhelpers/testhelpers_test.go b/testhelpers/testhelpers_test.go index ce328f01..62d45856 100644 --- a/testhelpers/testhelpers_test.go +++ b/testhelpers/testhelpers_test.go @@ -25,7 +25,7 @@ import ( "golang.org/x/sys/unix" - "github.com/appc/cni/pkg/testhelpers" + "github.com/containernetworking/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/types/args_test.go b/types/args_test.go index 23aaf817..61fd2233 100644 --- a/types/args_test.go +++ b/types/args_test.go @@ -17,7 +17,7 @@ package types_test import ( "reflect" - . "github.com/appc/cni/pkg/types" + . "github.com/containernetworking/cni/pkg/types" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" From 3358f16dd6cf2e4150833baed870bd7e91f401b8 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:28:10 -0700 Subject: [PATCH 048/134] Extract testhelpers from loopback test suite --- testhelpers/testhelpers.go | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 testhelpers/testhelpers.go diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go new file mode 100644 index 00000000..f1ccb24e --- /dev/null +++ b/testhelpers/testhelpers.go @@ -0,0 +1,69 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package testhelpers + +import ( + "fmt" + "os" + "runtime" + + "golang.org/x/sys/unix" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func MakeNetworkNS(containerID string) string { + namespace := "/var/run/netns/" + containerID + pid := unix.Getpid() + tid := unix.Gettid() + + err := os.MkdirAll("/var/run/netns", 0600) + Expect(err).NotTo(HaveOccurred()) + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + go (func() { + defer GinkgoRecover() + + err = unix.Unshare(unix.CLONE_NEWNET) + Expect(err).NotTo(HaveOccurred()) + + fd, err := os.Create(namespace) + Expect(err).NotTo(HaveOccurred()) + defer fd.Close() + + err = unix.Mount("/proc/self/ns/net", namespace, "none", unix.MS_BIND, "") + Expect(err).NotTo(HaveOccurred()) + })() + + Eventually(namespace).Should(BeAnExistingFile()) + + fd, err := unix.Open(fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid), unix.O_RDONLY, 0) + Expect(err).NotTo(HaveOccurred()) + + defer unix.Close(fd) + + _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(fd), uintptr(unix.CLONE_NEWNET), 0) + Expect(e1).To(BeZero()) + + return namespace +} + +func RemoveNetworkNS(networkNS string) error { + err := unix.Unmount(networkNS, unix.MNT_DETACH) + + err = os.RemoveAll(networkNS) + return err +} From bd7b83dd987a6a96cd806f7df84810f766264689 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:35:49 -0700 Subject: [PATCH 049/134] Extract inode inspection functions into testhelpers --- ns/ns_test.go | 38 +++++++++++--------------------------- testhelpers/testhelpers.go | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ns/ns_test.go b/ns/ns_test.go index d9f182cf..42fc6322 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -22,28 +22,12 @@ import ( "os/exec" "path/filepath" - "golang.org/x/sys/unix" - "github.com/appc/cni/pkg/ns" + "github.com/appc/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -func getInode(path string) (uint64, error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer file.Close() - return getInodeF(file) -} - -func getInodeF(file *os.File) (uint64, error) { - stat := &unix.Stat_t{} - err := unix.Fstat(int(file.Fd()), stat) - return stat.Ino, err -} - const CurrentNetNS = "/proc/self/ns/net" var _ = Describe("Linux namespace operations", func() { @@ -81,13 +65,13 @@ var _ = Describe("Linux namespace operations", func() { }) It("executes the callback within the target network namespace", func() { - expectedInode, err := getInode(targetNetNSPath) + expectedInode, err := testhelpers.GetInode(targetNetNSPath) Expect(err).NotTo(HaveOccurred()) var actualInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - actualInode, innerErr = getInode(CurrentNetNS) + actualInode, innerErr = testhelpers.GetInode(CurrentNetNS) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -97,13 +81,13 @@ var _ = Describe("Linux namespace operations", func() { }) It("provides the original namespace as the argument to the callback", func() { - hostNSInode, err := getInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) var inputNSInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(inputNS *os.File) error { - inputNSInode, err = getInodeF(inputNS) + inputNSInode, err = testhelpers.GetInodeF(inputNS) return nil }) Expect(err).NotTo(HaveOccurred()) @@ -113,7 +97,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("restores the calling thread to the original network namespace", func() { - preTestInode, err := getInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { @@ -121,7 +105,7 @@ var _ = Describe("Linux namespace operations", func() { }) Expect(err).NotTo(HaveOccurred()) - postTestInode, err := getInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -129,14 +113,14 @@ var _ = Describe("Linux namespace operations", func() { Context("when the callback returns an error", func() { It("restores the calling thread to the original namespace before returning", func() { - preTestInode, err := getInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { return errors.New("potato") }) - postTestInode, err := getInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -152,10 +136,10 @@ var _ = Describe("Linux namespace operations", func() { Describe("validating inode mapping to namespaces", func() { It("checks that different namespaces have different inodes", func() { - hostNSInode, err := getInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInode(CurrentNetNS) Expect(err).NotTo(HaveOccurred()) - testNsInode, err := getInode(targetNetNSPath) + testNsInode, err := testhelpers.GetInode(targetNetNSPath) Expect(err).NotTo(HaveOccurred()) Expect(hostNSInode).NotTo(Equal(0)) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index f1ccb24e..4eb42bdc 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -11,6 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +// Package testhelpers provides common support behavior for tests package testhelpers import ( @@ -24,6 +26,21 @@ import ( . "github.com/onsi/gomega" ) +func GetInode(path string) (uint64, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + return GetInodeF(file) +} + +func GetInodeF(file *os.File) (uint64, error) { + stat := &unix.Stat_t{} + err := unix.Fstat(int(file.Fd()), stat) + return stat.Ino, err +} + func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID pid := unix.Getpid() From c77f7431fbd388b4e55677d16b8cd9ef6aa77d1d Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 18:48:50 -0700 Subject: [PATCH 050/134] Add basic unit tests of testhelpers --- testhelpers/testhelpers_suite_test.go | 31 +++++++++ testhelpers/testhelpers_test.go | 96 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 testhelpers/testhelpers_suite_test.go create mode 100644 testhelpers/testhelpers_test.go diff --git a/testhelpers/testhelpers_suite_test.go b/testhelpers/testhelpers_suite_test.go new file mode 100644 index 00000000..88bfc3d6 --- /dev/null +++ b/testhelpers/testhelpers_suite_test.go @@ -0,0 +1,31 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testhelpers_test + +import ( + "math/rand" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTesthelpers(t *testing.T) { + rand.Seed(config.GinkgoConfig.RandomSeed) + RegisterFailHandler(Fail) + RunSpecs(t, "Testhelpers Suite") +} diff --git a/testhelpers/testhelpers_test.go b/testhelpers/testhelpers_test.go new file mode 100644 index 00000000..ce328f01 --- /dev/null +++ b/testhelpers/testhelpers_test.go @@ -0,0 +1,96 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testhelpers_test contains unit tests of the testhelpers +// +// Some of this stuff is non-trivial and can interact in surprising ways +// with the Go runtime. Better be safe. +package testhelpers_test + +import ( + "fmt" + "math/rand" + "path/filepath" + + "golang.org/x/sys/unix" + + "github.com/appc/cni/pkg/testhelpers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test helper functions", func() { + Describe("MakeNetworkNS", func() { + It("should return the filepath to a network namespace", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + nsPath := testhelpers.MakeNetworkNS(containerID) + + Expect(nsPath).To(BeAnExistingFile()) + + testhelpers.RemoveNetworkNS(containerID) + }) + + It("should return a network namespace different from that of the caller", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + + By("discovering the inode of the current netns") + originalNetNSPath := currentNetNSPath() + originalNetNSInode, err := testhelpers.GetInode(originalNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("creating a new netns") + createdNetNSPath := testhelpers.MakeNetworkNS(containerID) + defer testhelpers.RemoveNetworkNS(createdNetNSPath) + + By("discovering the inode of the created netns") + createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("comparing the inodes") + Expect(createdNetNSInode).NotTo(Equal(originalNetNSInode)) + }) + + It("should not leak the new netns onto any threads in the process", func() { + containerID := fmt.Sprintf("c-%x", rand.Int31()) + + By("creating a new netns") + createdNetNSPath := testhelpers.MakeNetworkNS(containerID) + defer testhelpers.RemoveNetworkNS(createdNetNSPath) + + By("discovering the inode of the created netns") + createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) + Expect(err).NotTo(HaveOccurred()) + + By("comparing against the netns inode of every thread in the process") + for _, netnsPath := range allNetNSInCurrentProcess() { + netnsInode, err := testhelpers.GetInode(netnsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(netnsInode).NotTo(Equal(createdNetNSInode)) + } + }) + }) +}) + +func currentNetNSPath() string { + pid := unix.Getpid() + tid := unix.Gettid() + return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) +} + +func allNetNSInCurrentProcess() []string { + pid := unix.Getpid() + paths, err := filepath.Glob(fmt.Sprintf("/proc/%d/task/*/ns/net", pid)) + Expect(err).NotTo(HaveOccurred()) + return paths +} From 5260862f809f9c38623b0d4bf17356f40aabb49a Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 19:44:00 -0700 Subject: [PATCH 051/134] Fix issues with MakeNetworkNS test helper --- testhelpers/testhelpers.go | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 4eb42bdc..0963121d 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "runtime" + "sync" "golang.org/x/sys/unix" @@ -43,37 +44,51 @@ func GetInodeF(file *os.File) (uint64, error) { func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID - pid := unix.Getpid() - tid := unix.Gettid() err := os.MkdirAll("/var/run/netns", 0600) Expect(err).NotTo(HaveOccurred()) - runtime.LockOSThread() - defer runtime.UnlockOSThread() + // create an empty file at the mount point + mountPointFd, err := os.Create(namespace) + Expect(err).NotTo(HaveOccurred()) + mountPointFd.Close() + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function go (func() { + defer wg.Done() + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + defer GinkgoRecover() + // capture current thread's original netns + pid := unix.Getpid() + tid := unix.Gettid() + currentThreadNetNSPath := fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) + originalNetNS, err := unix.Open(currentThreadNetNSPath, unix.O_RDONLY, 0) + Expect(err).NotTo(HaveOccurred()) + defer unix.Close(originalNetNS) + + // create a new netns on the current thread err = unix.Unshare(unix.CLONE_NEWNET) Expect(err).NotTo(HaveOccurred()) - fd, err := os.Create(namespace) + // bind mount the new netns from the current thread onto the mount point + err = unix.Mount(currentThreadNetNSPath, namespace, "none", unix.MS_BIND, "") Expect(err).NotTo(HaveOccurred()) - defer fd.Close() - err = unix.Mount("/proc/self/ns/net", namespace, "none", unix.MS_BIND, "") - Expect(err).NotTo(HaveOccurred()) + // reset current thread's netns to the original + _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(originalNetNS), uintptr(unix.CLONE_NEWNET), 0) + Expect(e1).To(BeZero()) })() - Eventually(namespace).Should(BeAnExistingFile()) - - fd, err := unix.Open(fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid), unix.O_RDONLY, 0) - Expect(err).NotTo(HaveOccurred()) - - defer unix.Close(fd) - - _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(fd), uintptr(unix.CLONE_NEWNET), 0) - Expect(e1).To(BeZero()) + wg.Wait() return namespace } From 5158edacef2b0be55136def257e967a4871ef657 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 6 Apr 2016 11:03:31 -0500 Subject: [PATCH 052/134] ns: fix reading net namespace in multi-threaded processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /proc/self/ns/net gives the main thread's namespace, not necessarily the namespace of the thread that's running the testcases. This causes sporadic failures of the tests. For example, with a testcase reading inodes after switching netns: /proc/27686/task/27689/ns/net 4026532565 /proc/self/ns/net 4026531969 /proc/27686/task/27689/ns/net 4026532565 See also: https://github.com/vishvananda/netns/commit/008d17ae001344769b031375bdb38a86219154c6 Running Suite: pkg/ns Suite =========================== Random Seed: 1459953577 Will run 6 of 6 specs • Failure [0.028 seconds] Linux namespace operations /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:167 WithNetNS /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:166 executes the callback within the target network namespace [It] /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:97 Expected : 4026531969 to equal : 4026532565 /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96 ------------------------------ ••••• Summarizing 1 Failure: [Fail] Linux namespace operations WithNetNS [It] executes the callback within the target network namespace /cni/gopath/src/github.com/appc/cni/pkg/ns/ns_test.go:96 Ran 6 of 6 Specs in 0.564 seconds FAIL! -- 5 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestNs (0.56s) FAIL --- ns/ns_test.go | 22 +++++++--------------- testhelpers/testhelpers.go | 14 +++++++++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/ns/ns_test.go b/ns/ns_test.go index 42fc6322..7ad882f5 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -28,13 +28,9 @@ import ( . "github.com/onsi/gomega" ) -const CurrentNetNS = "/proc/self/ns/net" - var _ = Describe("Linux namespace operations", func() { Describe("WithNetNS", func() { var ( - originalNetNS *os.File - targetNetNSName string targetNetNSPath string targetNetNS *os.File @@ -42,8 +38,6 @@ var _ = Describe("Linux namespace operations", func() { BeforeEach(func() { var err error - originalNetNS, err = os.Open(CurrentNetNS) - Expect(err).NotTo(HaveOccurred()) targetNetNSName = fmt.Sprintf("test-netns-%d", rand.Int()) @@ -60,8 +54,6 @@ var _ = Describe("Linux namespace operations", func() { err := exec.Command("ip", "netns", "del", targetNetNSName).Run() Expect(err).NotTo(HaveOccurred()) - - Expect(originalNetNS.Close()).To(Succeed()) }) It("executes the callback within the target network namespace", func() { @@ -71,7 +63,7 @@ var _ = Describe("Linux namespace operations", func() { var actualInode uint64 var innerErr error err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - actualInode, innerErr = testhelpers.GetInode(CurrentNetNS) + actualInode, innerErr = testhelpers.GetInodeCurNetNS() return nil }) Expect(err).NotTo(HaveOccurred()) @@ -81,7 +73,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("provides the original namespace as the argument to the callback", func() { - hostNSInode, err := testhelpers.GetInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) var inputNSInode uint64 @@ -97,7 +89,7 @@ var _ = Describe("Linux namespace operations", func() { }) It("restores the calling thread to the original network namespace", func() { - preTestInode, err := testhelpers.GetInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { @@ -105,7 +97,7 @@ var _ = Describe("Linux namespace operations", func() { }) Expect(err).NotTo(HaveOccurred()) - postTestInode, err := testhelpers.GetInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -113,14 +105,14 @@ var _ = Describe("Linux namespace operations", func() { Context("when the callback returns an error", func() { It("restores the calling thread to the original namespace before returning", func() { - preTestInode, err := testhelpers.GetInode(CurrentNetNS) + preTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { return errors.New("potato") }) - postTestInode, err := testhelpers.GetInode(CurrentNetNS) + postTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) Expect(postTestInode).To(Equal(preTestInode)) @@ -136,7 +128,7 @@ var _ = Describe("Linux namespace operations", func() { Describe("validating inode mapping to namespaces", func() { It("checks that different namespaces have different inodes", func() { - hostNSInode, err := testhelpers.GetInode(CurrentNetNS) + hostNSInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) testNsInode, err := testhelpers.GetInode(targetNetNSPath) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 0963121d..004006a9 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -27,6 +27,16 @@ import ( . "github.com/onsi/gomega" ) +func getCurrentThreadNetNSPath() string { + pid := unix.Getpid() + tid := unix.Gettid() + return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) +} + +func GetInodeCurNetNS() (uint64, error) { + return GetInode(getCurrentThreadNetNSPath()) +} + func GetInode(path string) (uint64, error) { file, err := os.Open(path) if err != nil { @@ -68,9 +78,7 @@ func MakeNetworkNS(containerID string) string { defer GinkgoRecover() // capture current thread's original netns - pid := unix.Getpid() - tid := unix.Gettid() - currentThreadNetNSPath := fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) + currentThreadNetNSPath := getCurrentThreadNetNSPath() originalNetNS, err := unix.Open(currentThreadNetNSPath, unix.O_RDONLY, 0) Expect(err).NotTo(HaveOccurred()) defer unix.Close(originalNetNS) From 19850efc3ae3873eeb70b16aefce3c5fe6aea7c8 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 17 Apr 2016 20:27:02 -0700 Subject: [PATCH 053/134] Document use of goroutine and lockosthread in test helpers --- testhelpers/testhelpers.go | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 004006a9..e9a0fb3f 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -52,6 +52,33 @@ func GetInodeF(file *os.File) (uint64, error) { return stat.Ino, err } +/* +A note about goroutines, Linux namespaces and runtime.LockOSThread + +In Linux, network namespaces have thread affinity. + +In the Go language runtime, goroutines do not have affinity for OS threads. +The Go runtime scheduler moves goroutines around amongst OS threads. It +is supposed to be transparent to the Go programmer. + +In order to address cases where the programmer needs thread affinity, Go +provides runtime.LockOSThread and runtime.UnlockOSThread() + +However, the Go runtime does not reference count the Lock and Unlock calls. +Repeated calls to Lock will succeed, but the first call to Unlock will unlock +everything. Therefore, it is dangerous to hide a Lock/Unlock in a library +function, such as in this package. + +The code below, in MakeNetworkNS, avoids this problem by spinning up a new +Go routine specifically so that LockOSThread can be called on it. Thus +goroutine-thread affinity is maintained long enough to perform all the required +namespace operations. + +Because the LockOSThread call is performed inside this short-lived goroutine, +there is no effect either way on the caller's goroutine-thread affinity. + +* */ + func MakeNetworkNS(containerID string) string { namespace := "/var/run/netns/" + containerID @@ -68,7 +95,7 @@ func MakeNetworkNS(containerID string) string { // do namespace work in a dedicated goroutine, so that we can safely // Lock/Unlock OSThread without upsetting the lock/unlock state of - // the caller of this function + // the caller of this function. See block comment above. go (func() { defer wg.Done() From 5cbd217cbea8137895a5c2020caf0248e0083334 Mon Sep 17 00:00:00 2001 From: Piotr Skamruk Date: Mon, 7 Mar 2016 16:40:27 +0100 Subject: [PATCH 054/134] pkg/utils: add functions to work with sysctl --- utils/sysctl/sysctl_linux.go | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 utils/sysctl/sysctl_linux.go diff --git a/utils/sysctl/sysctl_linux.go b/utils/sysctl/sysctl_linux.go new file mode 100644 index 00000000..c0fba382 --- /dev/null +++ b/utils/sysctl/sysctl_linux.go @@ -0,0 +1,58 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// build +linux + +package sysctl + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" +) + +// Sysctl provides a method to set/get values from /proc/sys - in linux systems +// new interface to set/get values of variables formerly handled by sysctl syscall +// If optional `params` have only one string value - this function will +// set this value into coresponding sysctl variable +func Sysctl(name string, params ...string) (string, error) { + if len(params) > 1 { + return "", fmt.Errorf("unexcepted additional parameters") + } else if len(params) == 1 { + return setSysctl(name, params[0]) + } + return getSysctl(name) +} + +func getSysctl(name string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + data, err := ioutil.ReadFile(fullName) + if err != nil { + return "", err + } + + return string(data[:len(data)-1]), nil +} + +func setSysctl(name, value string) (string, error) { + fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) + fullName = filepath.Clean(fullName) + if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil { + return "", err + } + + return getSysctl(name) +} From fc229c50908a526b5dcf642fae27b285d6da40a0 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 4 May 2016 16:06:05 +0200 Subject: [PATCH 055/134] *: appc/cni -> containernetworking/cni The project has been moved so internally we simply rename everything. Consumers are recommended to update their vendored version of cni. --- invoke/delegate.go | 2 +- invoke/exec.go | 2 +- invoke/find_test.go | 2 +- ip/link.go | 2 +- ipam/ipam.go | 6 +++--- ns/ns_test.go | 4 ++-- skel/skel.go | 2 +- testhelpers/testhelpers_test.go | 2 +- types/args_test.go | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/invoke/delegate.go b/invoke/delegate.go index 0a8198c3..ddf1d172 100644 --- a/invoke/delegate.go +++ b/invoke/delegate.go @@ -19,7 +19,7 @@ import ( "os" "strings" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) func DelegateAdd(delegatePlugin string, netconf []byte) (*types.Result, error) { diff --git a/invoke/exec.go b/invoke/exec.go index 337bfcb8..a85eede6 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -21,7 +21,7 @@ import ( "os" "os/exec" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) func pluginErr(err error, output []byte) error { diff --git a/invoke/find_test.go b/invoke/find_test.go index 4135538d..be4cc2dd 100644 --- a/invoke/find_test.go +++ b/invoke/find_test.go @@ -19,7 +19,7 @@ import ( "io/ioutil" "path/filepath" - "github.com/appc/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/invoke" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/ip/link.go b/ip/link.go index 1ba529da..df168124 100644 --- a/ip/link.go +++ b/ip/link.go @@ -20,7 +20,7 @@ import ( "net" "os" - "github.com/appc/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/ns" "github.com/vishvananda/netlink" ) diff --git a/ipam/ipam.go b/ipam/ipam.go index f0adfb7f..d9fbff74 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -18,9 +18,9 @@ import ( "fmt" "os" - "github.com/appc/cni/pkg/invoke" - "github.com/appc/cni/pkg/ip" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/types" "github.com/vishvananda/netlink" ) diff --git a/ns/ns_test.go b/ns/ns_test.go index 7ad882f5..a901eb35 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -22,8 +22,8 @@ import ( "os/exec" "path/filepath" - "github.com/appc/cni/pkg/ns" - "github.com/appc/cni/pkg/testhelpers" + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/skel/skel.go b/skel/skel.go index 5c3532db..7347b078 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -22,7 +22,7 @@ import ( "log" "os" - "github.com/appc/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types" ) // CmdArgs captures all the arguments passed in to the plugin diff --git a/testhelpers/testhelpers_test.go b/testhelpers/testhelpers_test.go index ce328f01..62d45856 100644 --- a/testhelpers/testhelpers_test.go +++ b/testhelpers/testhelpers_test.go @@ -25,7 +25,7 @@ import ( "golang.org/x/sys/unix" - "github.com/appc/cni/pkg/testhelpers" + "github.com/containernetworking/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/types/args_test.go b/types/args_test.go index 23aaf817..61fd2233 100644 --- a/types/args_test.go +++ b/types/args_test.go @@ -17,7 +17,7 @@ package types_test import ( "reflect" - . "github.com/appc/cni/pkg/types" + . "github.com/containernetworking/cni/pkg/types" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" From 5978cf8b8832cce492d6c0ff8246a5d82ddc57c0 Mon Sep 17 00:00:00 2001 From: Angus Lees Date: Fri, 13 May 2016 17:18:39 +1000 Subject: [PATCH 056/134] pkg/ns: use correct syscall number on arm --- ns/ns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ns/ns.go b/ns/ns.go index 4f0814f1..ea848224 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -24,7 +24,7 @@ import ( var setNsMap = map[string]uintptr{ "386": 346, "amd64": 308, - "arm": 374, + "arm": 375, } // SetNS sets the network namespace on a target file. From 734bf1ba5bbb2e99feeb81a437ab7fff47d88873 Mon Sep 17 00:00:00 2001 From: Angus Lees Date: Fri, 13 May 2016 17:32:40 +1000 Subject: [PATCH 057/134] pkg/ns: evaluate syscall number at compile-time Previously this code used a run-time map lookup keyed by runtime.GOOS/GOARCH. This version uses conditional compilation to make this choice at compile time, giving immediate feedback for unsupported platforms. --- ns/consts_linux_386.go | 17 +++++++++++++++++ ns/consts_linux_amd64.go | 17 +++++++++++++++++ ns/consts_linux_arm.go | 17 +++++++++++++++++ ns/ns.go | 17 +---------------- 4 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 ns/consts_linux_386.go create mode 100644 ns/consts_linux_amd64.go create mode 100644 ns/consts_linux_arm.go diff --git a/ns/consts_linux_386.go b/ns/consts_linux_386.go new file mode 100644 index 00000000..fd6ed8a0 --- /dev/null +++ b/ns/consts_linux_386.go @@ -0,0 +1,17 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +const setNsNr = 346 diff --git a/ns/consts_linux_amd64.go b/ns/consts_linux_amd64.go new file mode 100644 index 00000000..a86a68a4 --- /dev/null +++ b/ns/consts_linux_amd64.go @@ -0,0 +1,17 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +const setNsNr = 308 diff --git a/ns/consts_linux_arm.go b/ns/consts_linux_arm.go new file mode 100644 index 00000000..5beaaf3f --- /dev/null +++ b/ns/consts_linux_arm.go @@ -0,0 +1,17 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +const setNsNr = 375 diff --git a/ns/ns.go b/ns/ns.go index ea848224..72533993 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -21,24 +21,9 @@ import ( "syscall" ) -var setNsMap = map[string]uintptr{ - "386": 346, - "amd64": 308, - "arm": 375, -} - // SetNS sets the network namespace on a target file. func SetNS(f *os.File, flags uintptr) error { - if runtime.GOOS != "linux" { - return fmt.Errorf("unsupported OS: %s", runtime.GOOS) - } - - trap, ok := setNsMap[runtime.GOARCH] - if !ok { - return fmt.Errorf("unsupported arch: %s", runtime.GOARCH) - } - - _, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0) + _, _, err := syscall.RawSyscall(setNsNr, f.Fd(), flags, 0) if err != 0 { return err } From f2922c5c1fa8f9378df55f281ba1fa664d8ea53d Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 5 Apr 2016 11:10:31 -0500 Subject: [PATCH 058/134] ns: add interface, use it, and fix thread-related namespace switch issues Add a namespace object interface for somewhat cleaner code when creating and switching between network namespaces. All created namespaces are now mounted in /var/run/netns to ensure they have persistent inodes and paths that can be passed around between plugin components without relying on the current namespace being correct. Also remove the thread-locking arguments from the ns package per https://github.com/appc/cni/issues/183 by doing all the namespace changes in a separate goroutine that locks/unlocks itself, instead of the caller having to track OS thread locking. --- ip/link.go | 6 +- ns/README.md | 31 ++++ ns/consts_linux_386.go | 17 -- ns/consts_linux_amd64.go | 17 -- ns/consts_linux_arm.go | 17 -- ns/ns.go | 247 +++++++++++++++++++++----- ns/ns_test.go | 156 ++++++++++------ testhelpers/testhelpers.go | 136 -------------- testhelpers/testhelpers_suite_test.go | 31 ---- testhelpers/testhelpers_test.go | 96 ---------- 10 files changed, 334 insertions(+), 420 deletions(-) create mode 100644 ns/README.md delete mode 100644 ns/consts_linux_386.go delete mode 100644 ns/consts_linux_amd64.go delete mode 100644 ns/consts_linux_arm.go delete mode 100644 testhelpers/testhelpers.go delete mode 100644 testhelpers/testhelpers_suite_test.go delete mode 100644 testhelpers/testhelpers_test.go diff --git a/ip/link.go b/ip/link.go index df168124..1b785672 100644 --- a/ip/link.go +++ b/ip/link.go @@ -81,7 +81,7 @@ func RandomVethName() (string, error) { // SetupVeth sets up a virtual ethernet link. // Should be in container netns, and will switch back to hostNS to set the host // veth end up. -func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { +func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) { var hostVethName string hostVethName, contVeth, err = makeVeth(contVethName, mtu) if err != nil { @@ -104,10 +104,10 @@ func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVet return } - err = ns.WithNetNS(hostNS, false, func(_ *os.File) error { + err = hostNS.Do(func(_ ns.NetNS) error { hostVeth, err := netlink.LinkByName(hostVethName) if err != nil { - return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Name(), err) + return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err) } if err = netlink.LinkSetUp(hostVeth); err != nil { diff --git a/ns/README.md b/ns/README.md new file mode 100644 index 00000000..e7b20c2f --- /dev/null +++ b/ns/README.md @@ -0,0 +1,31 @@ +### Namespaces, Threads, and Go +On Linux each OS thread can have a different network namespace. Go's thread scheduling model switches goroutines between OS threads based on OS thread load and whether the goroutine would block other goroutines. This can result in a goroutine switching network namespaces without notice and lead to errors in your code. + +### Namespace Switching +Switching namespaces with the `ns.Set()` method is not recommended without additional strategies to prevent unexpected namespace changes when your goroutines switch OS threads. + +Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine executes on its current OS thread and prevents any other goroutine from running in that thread until the locked one exits. Careful usage of `LockOSThread()` and goroutines can provide good control over which network namespace a given goroutine executes in. + +For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly. + +### Do() The Recommended Thing +The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example: + +```go +targetNs, err := ns.NewNS() +if err != nil { + return err +} +err = targetNs.Do(func(hostNs ns.NetNS) error { + dummy := &netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: "dummy0", + }, + } + return netlink.LinkAdd(dummy) +}) +``` + +### Further Reading + - https://github.com/golang/go/wiki/LockOSThread + - http://morsmachine.dk/go-scheduler diff --git a/ns/consts_linux_386.go b/ns/consts_linux_386.go deleted file mode 100644 index fd6ed8a0..00000000 --- a/ns/consts_linux_386.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ns - -const setNsNr = 346 diff --git a/ns/consts_linux_amd64.go b/ns/consts_linux_amd64.go deleted file mode 100644 index a86a68a4..00000000 --- a/ns/consts_linux_amd64.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ns - -const setNsNr = 308 diff --git a/ns/consts_linux_arm.go b/ns/consts_linux_arm.go deleted file mode 100644 index 5beaaf3f..00000000 --- a/ns/consts_linux_arm.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ns - -const setNsNr = 375 diff --git a/ns/ns.go b/ns/ns.go index 72533993..119a8ce8 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -15,17 +15,211 @@ package ns import ( + "crypto/rand" "fmt" "os" + "path" "runtime" - "syscall" + "sync" + + "golang.org/x/sys/unix" ) -// SetNS sets the network namespace on a target file. -func SetNS(f *os.File, flags uintptr) error { - _, _, err := syscall.RawSyscall(setNsNr, f.Fd(), flags, 0) - if err != 0 { - return err +type NetNS interface { + // Executes the passed closure in this object's network namespace, + // attemtping to restore the original namespace before returning. + // However, since each OS thread can have a different network namespace, + // and Go's thread scheduling is highly variable, callers cannot + // guarantee any specific namespace is set unless operations that + // require that namespace are wrapped with Do(). Also, no code called + // from Do() should call runtime.UnlockOSThread(), or the risk + // of executing code in an incorrect namespace will be greater. See + // https://github.com/golang/go/wiki/LockOSThread for further details. + Do(toRun func(NetNS) error) error + + // Sets the current network namespace to this object's network namespace. + // Note that since Go's thread scheduling is highly variable, callers + // cannot guarantee the requested namespace will be the current namespace + // after this function is called; to ensure this wrap operations that + // require the namespace with Do() instead. + Set() error + + // Returns the filesystem path representing this object's network namespace + Path() string + + // Returns a file descriptor representing this object's network namespace + Fd() uintptr + + // Cleans up this instance of the network namespace; if this instance + // is the last user the namespace will be destroyed + Close() error +} + +type netNS struct { + file *os.File + mounted bool +} + +func getCurrentThreadNetNSPath() string { + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread this goroutine is running on. Make sure we + // use the thread's net namespace since the thread is switching around + return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) +} + +// Returns an object representing the current OS thread's network namespace +func GetCurrentNS() (NetNS, error) { + return GetNS(getCurrentThreadNetNSPath()) +} + +// Returns an object representing the namespace referred to by @path +func GetNS(nspath string) (NetNS, error) { + fd, err := os.Open(nspath) + if err != nil { + return nil, err + } + return &netNS{file: fd}, nil +} + +// Creates a new persistent network namespace and returns an object +// representing that namespace, without switching to it +func NewNS() (NetNS, error) { + const nsRunDir = "/var/run/netns" + + b := make([]byte, 16) + _, err := rand.Reader.Read(b) + if err != nil { + return nil, fmt.Errorf("failed to generate random netns name: %v", err) + } + + err = os.MkdirAll(nsRunDir, 0755) + if err != nil { + return nil, err + } + + // create an empty file at the mount point + nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + nsPath := path.Join(nsRunDir, nsName) + mountPointFd, err := os.Create(nsPath) + if err != nil { + return nil, err + } + mountPointFd.Close() + + // Ensure the mount point is cleaned up on errors; if the namespace + // was successfully mounted this will have no effect because the file + // is in-use + defer os.RemoveAll(nsPath) + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function + var fd *os.File + go (func() { + defer wg.Done() + runtime.LockOSThread() + + var origNS NetNS + origNS, err = GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return + } + defer origNS.Close() + + // create a new netns on the current thread + err = unix.Unshare(unix.CLONE_NEWNET) + if err != nil { + return + } + defer origNS.Set() + + // bind mount the new netns from the current thread onto the mount point + err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") + if err != nil { + return + } + + fd, err = os.Open(nsPath) + if err != nil { + return + } + })() + wg.Wait() + + if err != nil { + unix.Unmount(nsPath, unix.MNT_DETACH) + return nil, fmt.Errorf("failed to create namespace: %v", err) + } + + return &netNS{file: fd, mounted: true}, nil +} + +func (ns *netNS) Path() string { + return ns.file.Name() +} + +func (ns *netNS) Fd() uintptr { + return ns.file.Fd() +} + +func (ns *netNS) Close() error { + ns.file.Close() + + if ns.mounted { + if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil { + return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err) + } + if err := os.RemoveAll(ns.file.Name()); err != nil { + return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err) + } + } + return nil +} + +func (ns *netNS) Do(toRun func(NetNS) error) error { + containedCall := func(hostNS NetNS) error { + threadNS, err := GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return fmt.Errorf("failed to open current netns: %v", err) + } + defer threadNS.Close() + + // switch to target namespace + if err = ns.Set(); err != nil { + return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err) + } + defer threadNS.Set() // switch back + + return toRun(hostNS) + } + + // save a handle to current network namespace + hostNS, err := GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return fmt.Errorf("Failed to open current namespace: %v", err) + } + defer hostNS.Close() + + var wg sync.WaitGroup + wg.Add(1) + + var innerError error + go func() { + defer wg.Done() + runtime.LockOSThread() + innerError = containedCall(hostNS) + }() + wg.Wait() + + return innerError +} + +func (ns *netNS) Set() error { + if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 { + return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) } return nil @@ -33,46 +227,11 @@ func SetNS(f *os.File, flags uintptr) error { // WithNetNSPath executes the passed closure under the given network // namespace, restoring the original namespace afterwards. -// Changing namespaces must be done on a goroutine that has been -// locked to an OS thread. If lockThread arg is true, this function -// locks the goroutine prior to change namespace and unlocks before -// returning -func WithNetNSPath(nspath string, lockThread bool, f func(*os.File) error) error { - ns, err := os.Open(nspath) +func WithNetNSPath(nspath string, toRun func(NetNS) error) error { + ns, err := GetNS(nspath) if err != nil { return fmt.Errorf("Failed to open %v: %v", nspath, err) } defer ns.Close() - return WithNetNS(ns, lockThread, f) -} - -// WithNetNS executes the passed closure under the given network -// namespace, restoring the original namespace afterwards. -// Changing namespaces must be done on a goroutine that has been -// locked to an OS thread. If lockThread arg is true, this function -// locks the goroutine prior to change namespace and unlocks before -// returning. If the closure returns an error, WithNetNS attempts to -// restore the original namespace before returning. -func WithNetNS(ns *os.File, lockThread bool, f func(*os.File) error) error { - if lockThread { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - } - // save a handle to current (host) network namespace - thisNS, err := os.Open("/proc/self/ns/net") - if err != nil { - return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err) - } - defer thisNS.Close() - - if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil { - return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err) - } - defer SetNS(thisNS, syscall.CLONE_NEWNET) // switch back - - if err = f(thisNS); err != nil { - return err - } - - return nil + return ns.Do(toRun) } diff --git a/ns/ns_test.go b/ns/ns_test.go index a901eb35..836025e9 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -17,109 +17,123 @@ package ns_test import ( "errors" "fmt" - "math/rand" "os" - "os/exec" "path/filepath" "github.com/containernetworking/cni/pkg/ns" - "github.com/containernetworking/cni/pkg/testhelpers" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "golang.org/x/sys/unix" ) +func getInodeCurNetNS() (uint64, error) { + curNS, err := ns.GetCurrentNS() + if err != nil { + return 0, err + } + defer curNS.Close() + return getInodeNS(curNS) +} + +func getInodeNS(netns ns.NetNS) (uint64, error) { + return getInodeFd(int(netns.Fd())) +} + +func getInode(path string) (uint64, error) { + file, err := os.Open(path) + if err != nil { + return 0, err + } + defer file.Close() + return getInodeFd(int(file.Fd())) +} + +func getInodeFd(fd int) (uint64, error) { + stat := &unix.Stat_t{} + err := unix.Fstat(fd, stat) + return stat.Ino, err +} + var _ = Describe("Linux namespace operations", func() { Describe("WithNetNS", func() { var ( - targetNetNSName string - targetNetNSPath string - targetNetNS *os.File + originalNetNS ns.NetNS + targetNetNS ns.NetNS ) BeforeEach(func() { var err error - targetNetNSName = fmt.Sprintf("test-netns-%d", rand.Int()) - - err = exec.Command("ip", "netns", "add", targetNetNSName).Run() + originalNetNS, err = ns.NewNS() Expect(err).NotTo(HaveOccurred()) - targetNetNSPath = filepath.Join("/var/run/netns/", targetNetNSName) - targetNetNS, err = os.Open(targetNetNSPath) + targetNetNS, err = ns.NewNS() Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(targetNetNS.Close()).To(Succeed()) - - err := exec.Command("ip", "netns", "del", targetNetNSName).Run() - Expect(err).NotTo(HaveOccurred()) + Expect(originalNetNS.Close()).To(Succeed()) }) It("executes the callback within the target network namespace", func() { - expectedInode, err := testhelpers.GetInode(targetNetNSPath) + expectedInode, err := getInodeNS(targetNetNS) Expect(err).NotTo(HaveOccurred()) - var actualInode uint64 - var innerErr error - err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - actualInode, innerErr = testhelpers.GetInodeCurNetNS() + err = targetNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + actualInode, err := getInodeCurNetNS() + Expect(err).NotTo(HaveOccurred()) + Expect(actualInode).To(Equal(expectedInode)) return nil }) Expect(err).NotTo(HaveOccurred()) - - Expect(innerErr).NotTo(HaveOccurred()) - Expect(actualInode).To(Equal(expectedInode)) }) It("provides the original namespace as the argument to the callback", func() { - hostNSInode, err := testhelpers.GetInodeCurNetNS() - Expect(err).NotTo(HaveOccurred()) + // Ensure we start in originalNetNS + err := originalNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() - var inputNSInode uint64 - var innerErr error - err = ns.WithNetNS(targetNetNS, false, func(inputNS *os.File) error { - inputNSInode, err = testhelpers.GetInodeF(inputNS) + origNSInode, err := getInodeNS(originalNetNS) + Expect(err).NotTo(HaveOccurred()) + + err = targetNetNS.Do(func(hostNS ns.NetNS) error { + defer GinkgoRecover() + + hostNSInode, err := getInodeNS(hostNS) + Expect(err).NotTo(HaveOccurred()) + Expect(hostNSInode).To(Equal(origNSInode)) + return nil + }) return nil }) Expect(err).NotTo(HaveOccurred()) - - Expect(innerErr).NotTo(HaveOccurred()) - Expect(inputNSInode).To(Equal(hostNSInode)) - }) - - It("restores the calling thread to the original network namespace", func() { - preTestInode, err := testhelpers.GetInodeCurNetNS() - Expect(err).NotTo(HaveOccurred()) - - err = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - return nil - }) - Expect(err).NotTo(HaveOccurred()) - - postTestInode, err := testhelpers.GetInodeCurNetNS() - Expect(err).NotTo(HaveOccurred()) - - Expect(postTestInode).To(Equal(preTestInode)) }) Context("when the callback returns an error", func() { It("restores the calling thread to the original namespace before returning", func() { - preTestInode, err := testhelpers.GetInodeCurNetNS() - Expect(err).NotTo(HaveOccurred()) + err := originalNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() - _ = ns.WithNetNS(targetNetNS, false, func(*os.File) error { - return errors.New("potato") + preTestInode, err := getInodeCurNetNS() + Expect(err).NotTo(HaveOccurred()) + + _ = targetNetNS.Do(func(ns.NetNS) error { + return errors.New("potato") + }) + + postTestInode, err := getInodeCurNetNS() + Expect(err).NotTo(HaveOccurred()) + Expect(postTestInode).To(Equal(preTestInode)) + return nil }) - - postTestInode, err := testhelpers.GetInodeCurNetNS() Expect(err).NotTo(HaveOccurred()) - - Expect(postTestInode).To(Equal(preTestInode)) }) It("returns the error from the callback", func() { - err := ns.WithNetNS(targetNetNS, false, func(*os.File) error { + err := targetNetNS.Do(func(ns.NetNS) error { return errors.New("potato") }) Expect(err).To(MatchError("potato")) @@ -128,16 +142,40 @@ var _ = Describe("Linux namespace operations", func() { Describe("validating inode mapping to namespaces", func() { It("checks that different namespaces have different inodes", func() { - hostNSInode, err := testhelpers.GetInodeCurNetNS() + origNSInode, err := getInodeNS(originalNetNS) Expect(err).NotTo(HaveOccurred()) - testNsInode, err := testhelpers.GetInode(targetNetNSPath) + testNsInode, err := getInodeNS(targetNetNS) Expect(err).NotTo(HaveOccurred()) - Expect(hostNSInode).NotTo(Equal(0)) Expect(testNsInode).NotTo(Equal(0)) - Expect(testNsInode).NotTo(Equal(hostNSInode)) + Expect(testNsInode).NotTo(Equal(origNSInode)) + }) + + It("should not leak a closed netns onto any threads in the process", func() { + By("creating a new netns") + createdNetNS, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + By("discovering the inode of the created netns") + createdNetNSInode, err := getInodeNS(createdNetNS) + Expect(err).NotTo(HaveOccurred()) + createdNetNS.Close() + + By("comparing against the netns inode of every thread in the process") + for _, netnsPath := range allNetNSInCurrentProcess() { + netnsInode, err := getInode(netnsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(netnsInode).NotTo(Equal(createdNetNSInode)) + } }) }) }) }) + +func allNetNSInCurrentProcess() []string { + pid := unix.Getpid() + paths, err := filepath.Glob(fmt.Sprintf("/proc/%d/task/*/ns/net", pid)) + Expect(err).NotTo(HaveOccurred()) + return paths +} diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go deleted file mode 100644 index e9a0fb3f..00000000 --- a/testhelpers/testhelpers.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package testhelpers provides common support behavior for tests -package testhelpers - -import ( - "fmt" - "os" - "runtime" - "sync" - - "golang.org/x/sys/unix" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func getCurrentThreadNetNSPath() string { - pid := unix.Getpid() - tid := unix.Gettid() - return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) -} - -func GetInodeCurNetNS() (uint64, error) { - return GetInode(getCurrentThreadNetNSPath()) -} - -func GetInode(path string) (uint64, error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer file.Close() - return GetInodeF(file) -} - -func GetInodeF(file *os.File) (uint64, error) { - stat := &unix.Stat_t{} - err := unix.Fstat(int(file.Fd()), stat) - return stat.Ino, err -} - -/* -A note about goroutines, Linux namespaces and runtime.LockOSThread - -In Linux, network namespaces have thread affinity. - -In the Go language runtime, goroutines do not have affinity for OS threads. -The Go runtime scheduler moves goroutines around amongst OS threads. It -is supposed to be transparent to the Go programmer. - -In order to address cases where the programmer needs thread affinity, Go -provides runtime.LockOSThread and runtime.UnlockOSThread() - -However, the Go runtime does not reference count the Lock and Unlock calls. -Repeated calls to Lock will succeed, but the first call to Unlock will unlock -everything. Therefore, it is dangerous to hide a Lock/Unlock in a library -function, such as in this package. - -The code below, in MakeNetworkNS, avoids this problem by spinning up a new -Go routine specifically so that LockOSThread can be called on it. Thus -goroutine-thread affinity is maintained long enough to perform all the required -namespace operations. - -Because the LockOSThread call is performed inside this short-lived goroutine, -there is no effect either way on the caller's goroutine-thread affinity. - -* */ - -func MakeNetworkNS(containerID string) string { - namespace := "/var/run/netns/" + containerID - - err := os.MkdirAll("/var/run/netns", 0600) - Expect(err).NotTo(HaveOccurred()) - - // create an empty file at the mount point - mountPointFd, err := os.Create(namespace) - Expect(err).NotTo(HaveOccurred()) - mountPointFd.Close() - - var wg sync.WaitGroup - wg.Add(1) - - // do namespace work in a dedicated goroutine, so that we can safely - // Lock/Unlock OSThread without upsetting the lock/unlock state of - // the caller of this function. See block comment above. - go (func() { - defer wg.Done() - - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - defer GinkgoRecover() - - // capture current thread's original netns - currentThreadNetNSPath := getCurrentThreadNetNSPath() - originalNetNS, err := unix.Open(currentThreadNetNSPath, unix.O_RDONLY, 0) - Expect(err).NotTo(HaveOccurred()) - defer unix.Close(originalNetNS) - - // create a new netns on the current thread - err = unix.Unshare(unix.CLONE_NEWNET) - Expect(err).NotTo(HaveOccurred()) - - // bind mount the new netns from the current thread onto the mount point - err = unix.Mount(currentThreadNetNSPath, namespace, "none", unix.MS_BIND, "") - Expect(err).NotTo(HaveOccurred()) - - // reset current thread's netns to the original - _, _, e1 := unix.Syscall(unix.SYS_SETNS, uintptr(originalNetNS), uintptr(unix.CLONE_NEWNET), 0) - Expect(e1).To(BeZero()) - })() - - wg.Wait() - - return namespace -} - -func RemoveNetworkNS(networkNS string) error { - err := unix.Unmount(networkNS, unix.MNT_DETACH) - - err = os.RemoveAll(networkNS) - return err -} diff --git a/testhelpers/testhelpers_suite_test.go b/testhelpers/testhelpers_suite_test.go deleted file mode 100644 index 88bfc3d6..00000000 --- a/testhelpers/testhelpers_suite_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testhelpers_test - -import ( - "math/rand" - - . "github.com/onsi/ginkgo" - "github.com/onsi/ginkgo/config" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTesthelpers(t *testing.T) { - rand.Seed(config.GinkgoConfig.RandomSeed) - RegisterFailHandler(Fail) - RunSpecs(t, "Testhelpers Suite") -} diff --git a/testhelpers/testhelpers_test.go b/testhelpers/testhelpers_test.go deleted file mode 100644 index 62d45856..00000000 --- a/testhelpers/testhelpers_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package testhelpers_test contains unit tests of the testhelpers -// -// Some of this stuff is non-trivial and can interact in surprising ways -// with the Go runtime. Better be safe. -package testhelpers_test - -import ( - "fmt" - "math/rand" - "path/filepath" - - "golang.org/x/sys/unix" - - "github.com/containernetworking/cni/pkg/testhelpers" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Test helper functions", func() { - Describe("MakeNetworkNS", func() { - It("should return the filepath to a network namespace", func() { - containerID := fmt.Sprintf("c-%x", rand.Int31()) - nsPath := testhelpers.MakeNetworkNS(containerID) - - Expect(nsPath).To(BeAnExistingFile()) - - testhelpers.RemoveNetworkNS(containerID) - }) - - It("should return a network namespace different from that of the caller", func() { - containerID := fmt.Sprintf("c-%x", rand.Int31()) - - By("discovering the inode of the current netns") - originalNetNSPath := currentNetNSPath() - originalNetNSInode, err := testhelpers.GetInode(originalNetNSPath) - Expect(err).NotTo(HaveOccurred()) - - By("creating a new netns") - createdNetNSPath := testhelpers.MakeNetworkNS(containerID) - defer testhelpers.RemoveNetworkNS(createdNetNSPath) - - By("discovering the inode of the created netns") - createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) - Expect(err).NotTo(HaveOccurred()) - - By("comparing the inodes") - Expect(createdNetNSInode).NotTo(Equal(originalNetNSInode)) - }) - - It("should not leak the new netns onto any threads in the process", func() { - containerID := fmt.Sprintf("c-%x", rand.Int31()) - - By("creating a new netns") - createdNetNSPath := testhelpers.MakeNetworkNS(containerID) - defer testhelpers.RemoveNetworkNS(createdNetNSPath) - - By("discovering the inode of the created netns") - createdNetNSInode, err := testhelpers.GetInode(createdNetNSPath) - Expect(err).NotTo(HaveOccurred()) - - By("comparing against the netns inode of every thread in the process") - for _, netnsPath := range allNetNSInCurrentProcess() { - netnsInode, err := testhelpers.GetInode(netnsPath) - Expect(err).NotTo(HaveOccurred()) - Expect(netnsInode).NotTo(Equal(createdNetNSInode)) - } - }) - }) -}) - -func currentNetNSPath() string { - pid := unix.Getpid() - tid := unix.Gettid() - return fmt.Sprintf("/proc/%d/task/%d/ns/net", pid, tid) -} - -func allNetNSInCurrentProcess() []string { - pid := unix.Getpid() - paths, err := filepath.Glob(fmt.Sprintf("/proc/%d/task/*/ns/net", pid)) - Expect(err).NotTo(HaveOccurred()) - return paths -} From fce81fbf78287b98ab51d9acd40ff0e85ef292f1 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 29 Mar 2016 17:45:46 -0500 Subject: [PATCH 059/134] testutils: add e2e testing helper code --- testutils/cmd.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 testutils/cmd.go diff --git a/testutils/cmd.go b/testutils/cmd.go new file mode 100644 index 00000000..201b935f --- /dev/null +++ b/testutils/cmd.go @@ -0,0 +1,77 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "encoding/json" + "io/ioutil" + "os" + + "github.com/containernetworking/cni/pkg/types" +) + +func envCleanup() { + os.Unsetenv("CNI_COMMAND") + os.Unsetenv("CNI_PATH") + os.Unsetenv("CNI_NETNS") + os.Unsetenv("CNI_IFNAME") +} + +func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result, error) { + os.Setenv("CNI_COMMAND", "ADD") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + defer envCleanup() + + // Redirect stdout to capture plugin result + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + return nil, err + } + + os.Stdout = w + err = f() + w.Close() + if err != nil { + return nil, err + } + + // parse the result + out, err := ioutil.ReadAll(r) + os.Stdout = oldStdout + if err != nil { + return nil, err + } + + result := types.Result{} + err = json.Unmarshal(out, &result) + if err != nil { + return nil, err + } + + return &result, nil +} + +func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { + os.Setenv("CNI_COMMAND", "DEL") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + defer envCleanup() + + return f() +} From 40440ec88799a86485afac17a20a1f0116844970 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 24 May 2016 20:27:18 +0200 Subject: [PATCH 060/134] pkg/ns: don't allow operations after Close() --- ns/ns.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ns/ns.go b/ns/ns.go index 119a8ce8..837ab8be 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -58,6 +58,7 @@ type NetNS interface { type netNS struct { file *os.File mounted bool + closed bool } func getCurrentThreadNetNSPath() string { @@ -165,8 +166,22 @@ func (ns *netNS) Fd() uintptr { return ns.file.Fd() } +func (ns *netNS) errorIfClosed() error { + if ns.closed { + return fmt.Errorf("%q has already been closed", ns.file.Name()) + } + return nil +} + func (ns *netNS) Close() error { - ns.file.Close() + if err := ns.errorIfClosed(); err != nil { + return err + } + + if err := ns.file.Close(); err != nil { + return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) + } + ns.closed = true if ns.mounted { if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil { @@ -175,11 +190,17 @@ func (ns *netNS) Close() error { if err := os.RemoveAll(ns.file.Name()); err != nil { return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err) } + ns.mounted = false } + return nil } func (ns *netNS) Do(toRun func(NetNS) error) error { + if err := ns.errorIfClosed(); err != nil { + return err + } + containedCall := func(hostNS NetNS) error { threadNS, err := GetNS(getCurrentThreadNetNSPath()) if err != nil { @@ -218,6 +239,10 @@ func (ns *netNS) Do(toRun func(NetNS) error) error { } func (ns *netNS) Set() error { + if err := ns.errorIfClosed(); err != nil { + return err + } + if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 { return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) } From a8d102929476f9008baae8decfe95a57de2d9649 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 24 May 2016 20:48:12 +0200 Subject: [PATCH 061/134] pkg/ns: add tests cases for Close()'d NS --- ns/ns_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ns/ns_test.go b/ns/ns_test.go index 836025e9..de0f3853 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -170,6 +170,33 @@ var _ = Describe("Linux namespace operations", func() { } }) }) + + Describe("closing a network namespace", func() { + It("should prevent further operations", func() { + createdNetNS, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = createdNetNS.Close() + Expect(err).NotTo(HaveOccurred()) + + err = createdNetNS.Do(func(ns.NetNS) error { return nil }) + Expect(err).To(HaveOccurred()) + + err = createdNetNS.Set() + Expect(err).To(HaveOccurred()) + }) + + It("should only work once", func() { + createdNetNS, err := ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = createdNetNS.Close() + Expect(err).NotTo(HaveOccurred()) + + err = createdNetNS.Close() + Expect(err).To(HaveOccurred()) + }) + }) }) }) From 39eace3c38dcd55bd88db802fde272c84448de1a Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 24 May 2016 18:16:34 +0200 Subject: [PATCH 062/134] pkg/ns: verify netns when initialized with GetNS --- ns/ns.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ns/ns.go b/ns/ns.go index 837ab8be..328fff34 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -21,6 +21,7 @@ import ( "path" "runtime" "sync" + "syscall" "golang.org/x/sys/unix" ) @@ -77,11 +78,34 @@ func GetCurrentNS() (NetNS, error) { func GetNS(nspath string) (NetNS, error) { fd, err := os.Open(nspath) if err != nil { + return nil, fmt.Errorf("Failed to open %v: %v", nspath, err) + } + + isNSFS, err := IsNSFS(nspath) + if err != nil { + fd.Close() return nil, err } + if !isNSFS { + fd.Close() + return nil, fmt.Errorf("%v is not of type NSFS", nspath) + } + return &netNS{file: fd}, nil } +// Returns whether or not the nspath argument points to a network namespace +func IsNSFS(nspath string) (bool, error) { + const NSFS_MAGIC = 0x6e736673 + + stat := syscall.Statfs_t{} + if err := syscall.Statfs(nspath, &stat); err != nil { + return false, fmt.Errorf("failed to Statfs %q: %v", nspath, err) + } + + return stat.Type == NSFS_MAGIC, nil +} + // Creates a new persistent network namespace and returns an object // representing that namespace, without switching to it func NewNS() (NetNS, error) { @@ -255,7 +279,7 @@ func (ns *netNS) Set() error { func WithNetNSPath(nspath string, toRun func(NetNS) error) error { ns, err := GetNS(nspath) if err != nil { - return fmt.Errorf("Failed to open %v: %v", nspath, err) + return err } defer ns.Close() return ns.Do(toRun) From f64cfb84f836ba4401c11c6cd10dbff2c5fe3dd8 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 24 May 2016 22:21:57 +0200 Subject: [PATCH 063/134] pkg/ns: test case for rejecting a non-ns nspath --- ns/ns_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ns/ns_test.go b/ns/ns_test.go index de0f3853..d89e6936 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -17,6 +17,7 @@ package ns_test import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" @@ -169,6 +170,18 @@ var _ = Describe("Linux namespace operations", func() { Expect(netnsInode).NotTo(Equal(createdNetNSInode)) } }) + + It("fails when the path is not a namespace", func() { + tempFile, err := ioutil.TempFile("", "nstest") + Expect(err).NotTo(HaveOccurred()) + defer tempFile.Close() + + nspath := tempFile.Name() + defer os.Remove(nspath) + + _, err = ns.GetNS(nspath) + Expect(err).To(MatchError(fmt.Sprintf("%v is not of type NSFS", nspath))) + }) }) Describe("closing a network namespace", func() { From ea073fc4c7ead36fc3976a5fb6e80d779f4920b1 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 24 May 2016 22:30:01 +0200 Subject: [PATCH 064/134] pkg/ns: test IsNSFS() --- ns/ns_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ns/ns_test.go b/ns/ns_test.go index d89e6936..82001ea0 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -211,6 +211,28 @@ var _ = Describe("Linux namespace operations", func() { }) }) }) + + Describe("IsNSFS", func() { + It("should detect a namespace", func() { + createdNetNS, err := ns.NewNS() + isNSFS, err := ns.IsNSFS(createdNetNS.Path()) + Expect(err).NotTo(HaveOccurred()) + Expect(isNSFS).To(Equal(true)) + }) + + It("should refuse other paths", func() { + tempFile, err := ioutil.TempFile("", "nstest") + Expect(err).NotTo(HaveOccurred()) + defer tempFile.Close() + + nspath := tempFile.Name() + defer os.Remove(nspath) + + isNSFS, err := ns.IsNSFS(nspath) + Expect(err).NotTo(HaveOccurred()) + Expect(isNSFS).To(Equal(false)) + }) + }) }) func allNetNSInCurrentProcess() []string { From 2d47b0396d4b9d6483461ef93b665427f083c33d Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 25 May 2016 23:49:25 +0200 Subject: [PATCH 065/134] pkg/ns: consider PROCFS during NS verification This is an attempt to bring compatibility with Kernel <3.19, where NSFS where PROCFS was used for network namespaces. --- ns/ns.go | 65 ++++++++++++++++++++++++++++++++++----------------- ns/ns_test.go | 11 +++++---- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/ns/ns.go b/ns/ns.go index 328fff34..a03ee1e0 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -20,6 +20,7 @@ import ( "os" "path" "runtime" + "strings" "sync" "syscall" @@ -74,38 +75,58 @@ func GetCurrentNS() (NetNS, error) { return GetNS(getCurrentThreadNetNSPath()) } +const ( + // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h + NSFS_MAGIC = 0x6e736673 + PROCFS_MAGIC = 0x9fa0 +) + +func IsNS(nspath string) (isNS bool, msg string, err error) { + stat := syscall.Statfs_t{} + if err = syscall.Statfs(nspath, &stat); err != nil { + err = fmt.Errorf("failed to Statfs %s: %v", nspath, err) + return + } + + switch stat.Type { + case PROCFS_MAGIC: + // Kernel < 3.19 + + validPathContent := "ns/" + validName := strings.Contains(nspath, validPathContent) + if !validName { + msg = fmt.Sprintf("path doesn't contain %q", validPathContent) + return + } + isNS = true + case NSFS_MAGIC: + // Kernel >= 3.19 + + isNS = true + default: + msg = fmt.Sprintf("unknown FS magic: %x", stat.Type) + } + return +} + // Returns an object representing the namespace referred to by @path func GetNS(nspath string) (NetNS, error) { + isNS, msg, err := IsNS(nspath) + if err != nil { + return nil, err + } + if !isNS { + return nil, fmt.Errorf("no network namespace detected on %s: %s", nspath, msg) + } + fd, err := os.Open(nspath) if err != nil { return nil, fmt.Errorf("Failed to open %v: %v", nspath, err) } - isNSFS, err := IsNSFS(nspath) - if err != nil { - fd.Close() - return nil, err - } - if !isNSFS { - fd.Close() - return nil, fmt.Errorf("%v is not of type NSFS", nspath) - } - return &netNS{file: fd}, nil } -// Returns whether or not the nspath argument points to a network namespace -func IsNSFS(nspath string) (bool, error) { - const NSFS_MAGIC = 0x6e736673 - - stat := syscall.Statfs_t{} - if err := syscall.Statfs(nspath, &stat); err != nil { - return false, fmt.Errorf("failed to Statfs %q: %v", nspath, err) - } - - return stat.Type == NSFS_MAGIC, nil -} - // Creates a new persistent network namespace and returns an object // representing that namespace, without switching to it func NewNS() (NetNS, error) { diff --git a/ns/ns_test.go b/ns/ns_test.go index 82001ea0..5b0b587d 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -180,7 +180,9 @@ var _ = Describe("Linux namespace operations", func() { defer os.Remove(nspath) _, err = ns.GetNS(nspath) - Expect(err).To(MatchError(fmt.Sprintf("%v is not of type NSFS", nspath))) + Expect(err).To(HaveOccurred()) + errString := fmt.Sprintf("%v", err) + Expect(errString).To(HavePrefix("no network namespace detected on %s", nspath)) }) }) @@ -212,11 +214,12 @@ var _ = Describe("Linux namespace operations", func() { }) }) - Describe("IsNSFS", func() { + Describe("IsNS", func() { It("should detect a namespace", func() { createdNetNS, err := ns.NewNS() - isNSFS, err := ns.IsNSFS(createdNetNS.Path()) + isNSFS, msg, err := ns.IsNS(createdNetNS.Path()) Expect(err).NotTo(HaveOccurred()) + Expect(msg).To(Equal("")) Expect(isNSFS).To(Equal(true)) }) @@ -228,7 +231,7 @@ var _ = Describe("Linux namespace operations", func() { nspath := tempFile.Name() defer os.Remove(nspath) - isNSFS, err := ns.IsNSFS(nspath) + isNSFS, _, err := ns.IsNS(nspath) Expect(err).NotTo(HaveOccurred()) Expect(isNSFS).To(Equal(false)) }) From d7de8d4f98186b67d7a7b6568c13be4401fa0938 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 27 May 2016 10:56:24 +0200 Subject: [PATCH 066/134] pkg/skel: allow arg requriements specified by CMD --- skel/skel.go | 64 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/skel/skel.go b/skel/skel.go index 7347b078..1f9438c1 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -36,28 +36,72 @@ type CmdArgs struct { StdinData []byte } +type reqForCmdEntry map[string]bool + // PluginMain is the "main" for a plugin. It accepts // two callback functions for add and del commands. func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { var cmd, contID, netns, ifName, args, path string vars := []struct { - name string - val *string - req bool + name string + val *string + reqForCmd reqForCmdEntry }{ - {"CNI_COMMAND", &cmd, true}, - {"CNI_CONTAINERID", &contID, false}, - {"CNI_NETNS", &netns, true}, - {"CNI_IFNAME", &ifName, true}, - {"CNI_ARGS", &args, false}, - {"CNI_PATH", &path, true}, + { + "CNI_COMMAND", + &cmd, + reqForCmdEntry{ + "ADD": true, + "DEL": true, + }, + }, + { + "CNI_CONTAINERID", + &contID, + reqForCmdEntry{ + "ADD": false, + "DEL": false, + }, + }, + { + "CNI_NETNS", + &netns, + reqForCmdEntry{ + "ADD": true, + "DEL": true, + }, + }, + { + "CNI_IFNAME", + &ifName, + reqForCmdEntry{ + "ADD": true, + "DEL": true, + }, + }, + { + "CNI_ARGS", + &args, + reqForCmdEntry{ + "ADD": false, + "DEL": false, + }, + }, + { + "CNI_PATH", + &path, + reqForCmdEntry{ + "ADD": true, + "DEL": true, + }, + }, } argsMissing := false for _, v := range vars { *v.val = os.Getenv(v.name) - if v.req && *v.val == "" { + if v.reqForCmd[cmd] && *v.val == "" { log.Printf("%v env variable missing", v.name) argsMissing = true } From 5fe036b88ac070a0cfa3a2508847404ada9383bc Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 27 May 2016 10:57:39 +0200 Subject: [PATCH 067/134] plugins: don't require CNI_NETNS for DEL command This will allow to free up the IPAM allocations when the caller doesn't have access to the network namespace anymore, e.g. due to a reboot. --- skel/skel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skel/skel.go b/skel/skel.go index 1f9438c1..9cf03917 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -69,7 +69,7 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { &netns, reqForCmdEntry{ "ADD": true, - "DEL": true, + "DEL": false, }, }, { From cfedcfd8f23a58091d415c11885d1aafb0ceb6f6 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 27 May 2016 12:26:42 +0200 Subject: [PATCH 068/134] skel/test: add case for empty NETNS --- skel/skel_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skel/skel_test.go b/skel/skel_test.go index 44695648..a52e0145 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -71,5 +71,14 @@ var _ = Describe("Skel", func() { // Expect(err).NotTo(HaveOccurred()) // PluginMain(fErr, nil) // }) + + It("should not fail with DEL and no NETNS and noop callback", func() { + err := os.Setenv("CNI_COMMAND", "DEL") + Expect(err).NotTo(HaveOccurred()) + err = os.Unsetenv("CNI_NETNS") + Expect(err).NotTo(HaveOccurred()) + PluginMain(nil, fNoop) + }) + }) }) From 3a4124d257f32e8b1bc770c9806bf68a84afc8d2 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Fri, 27 May 2016 11:52:44 +0200 Subject: [PATCH 069/134] pkg/ns: introduce error types indicate NS verification --- ns/ns.go | 38 +++++++++++++++++++++++--------------- ns/ns_test.go | 24 +++++++++++++++--------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/ns/ns.go b/ns/ns.go index a03ee1e0..e29f712c 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -81,11 +81,23 @@ const ( PROCFS_MAGIC = 0x9fa0 ) -func IsNS(nspath string) (isNS bool, msg string, err error) { +type NSPathNotExistErr struct{ msg string } + +func (e NSPathNotExistErr) Error() string { return e.msg } + +type NSPathNotNSErr struct{ msg string } + +func (e NSPathNotNSErr) Error() string { return e.msg } + +func IsNSorErr(nspath string) error { stat := syscall.Statfs_t{} - if err = syscall.Statfs(nspath, &stat); err != nil { - err = fmt.Errorf("failed to Statfs %s: %v", nspath, err) - return + if err := syscall.Statfs(nspath, &stat); err != nil { + if os.IsNotExist(err) { + err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)} + } else { + err = fmt.Errorf("failed to Statfs %q: %v", nspath, err) + } + return err } switch stat.Type { @@ -95,33 +107,29 @@ func IsNS(nspath string) (isNS bool, msg string, err error) { validPathContent := "ns/" validName := strings.Contains(nspath, validPathContent) if !validName { - msg = fmt.Sprintf("path doesn't contain %q", validPathContent) - return + return NSPathNotNSErr{msg: fmt.Sprintf("path %q doesn't contain %q", nspath, validPathContent)} } - isNS = true + + return nil case NSFS_MAGIC: // Kernel >= 3.19 - isNS = true + return nil default: - msg = fmt.Sprintf("unknown FS magic: %x", stat.Type) + return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} } - return } // Returns an object representing the namespace referred to by @path func GetNS(nspath string) (NetNS, error) { - isNS, msg, err := IsNS(nspath) + err := IsNSorErr(nspath) if err != nil { return nil, err } - if !isNS { - return nil, fmt.Errorf("no network namespace detected on %s: %s", nspath, msg) - } fd, err := os.Open(nspath) if err != nil { - return nil, fmt.Errorf("Failed to open %v: %v", nspath, err) + return nil, err } return &netNS{file: fd}, nil diff --git a/ns/ns_test.go b/ns/ns_test.go index 5b0b587d..44ed2728 100644 --- a/ns/ns_test.go +++ b/ns/ns_test.go @@ -181,8 +181,8 @@ var _ = Describe("Linux namespace operations", func() { _, err = ns.GetNS(nspath) Expect(err).To(HaveOccurred()) - errString := fmt.Sprintf("%v", err) - Expect(errString).To(HavePrefix("no network namespace detected on %s", nspath)) + Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{})) + Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{})) }) }) @@ -214,13 +214,11 @@ var _ = Describe("Linux namespace operations", func() { }) }) - Describe("IsNS", func() { + Describe("IsNSorErr", func() { It("should detect a namespace", func() { createdNetNS, err := ns.NewNS() - isNSFS, msg, err := ns.IsNS(createdNetNS.Path()) + err = ns.IsNSorErr(createdNetNS.Path()) Expect(err).NotTo(HaveOccurred()) - Expect(msg).To(Equal("")) - Expect(isNSFS).To(Equal(true)) }) It("should refuse other paths", func() { @@ -231,9 +229,17 @@ var _ = Describe("Linux namespace operations", func() { nspath := tempFile.Name() defer os.Remove(nspath) - isNSFS, _, err := ns.IsNS(nspath) - Expect(err).NotTo(HaveOccurred()) - Expect(isNSFS).To(Equal(false)) + err = ns.IsNSorErr(nspath) + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotNSErr{})) + Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotExistErr{})) + }) + + It("should error on non-existing paths", func() { + err := ns.IsNSorErr("/tmp/IDoNotExist") + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(ns.NSPathNotExistErr{})) + Expect(err).NotTo(BeAssignableToTypeOf(ns.NSPathNotNSErr{})) }) }) }) From efc0f2987f852fb2ec6d62a38a64c1e131aec569 Mon Sep 17 00:00:00 2001 From: Tom Denham Date: Tue, 31 May 2016 17:12:40 -0700 Subject: [PATCH 070/134] pkg/types: Add UnmarshallableString type Allow strings to be unmarshalled for CNI_ARGS CNI_ARGS uses types.LoadArgs to populate a struct. The fields in the struct must meet the TextUnmarshaler interface. This code adds a UnmarshallableString type to assist with this. --- types/args.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/types/args.go b/types/args.go index 3b667b0f..66dcf9ea 100644 --- a/types/args.go +++ b/types/args.go @@ -41,6 +41,16 @@ func (b *UnmarshallableBool) UnmarshalText(data []byte) error { return nil } +// UnmarshallableString typedef for builtin string +type UnmarshallableString string + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Returns the string +func (s *UnmarshallableString) UnmarshalText(data []byte) error { + *s = UnmarshallableString(data) + return nil +} + // CommonArgs contains the IgnoreUnknown argument // and must be embedded by all Arg structs type CommonArgs struct { From d48cb537150feb530c64a84b23d8a723d82b93f7 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Mon, 6 Jun 2016 19:50:42 +0200 Subject: [PATCH 071/134] pkg/types: cover string for unmarshal tests --- types/args_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/types/args_test.go b/types/args_test.go index 61fd2233..3a53d9a4 100644 --- a/types/args_test.go +++ b/types/args_test.go @@ -47,6 +47,21 @@ var _ = Describe("UnmarshallableBool UnmarshalText", func() { }) }) +var _ = Describe("UnmarshallableString UnmarshalText", func() { + DescribeTable("string to string detection should succeed in all cases", + func(inputs []string, expected string) { + for _, s := range inputs { + var us UnmarshallableString + err := us.UnmarshalText([]byte(s)) + Expect(err).ToNot(HaveOccurred()) + Expect(string(us)).To(Equal(expected)) + } + }, + Entry("parse empty string", []string{""}, ""), + Entry("parse non-empty string", []string{"notempty"}, "notempty"), + ) +}) + var _ = Describe("GetKeyField", func() { type testcontainer struct { Valid string `json:"valid,omitempty"` From bd341ab5d14db54086e12de835fa2344ad403409 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 15 Jun 2016 00:52:58 +0800 Subject: [PATCH 072/134] pkg/ns: fix misspelling in comment --- ns/ns.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ns/ns.go b/ns/ns.go index e29f712c..320eed7b 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -29,7 +29,7 @@ import ( type NetNS interface { // Executes the passed closure in this object's network namespace, - // attemtping to restore the original namespace before returning. + // attempting to restore the original namespace before returning. // However, since each OS thread can have a different network namespace, // and Go's thread scheduling is highly variable, callers cannot // guarantee any specific namespace is set unless operations that From 3c433149260ceaf93be5c05ecf5e94617aa2e1a3 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Wed, 13 Jul 2016 22:12:06 -0400 Subject: [PATCH 073/134] pkg/skel: refactor to use dependency injection Extract dependencies on os to enable more complete unit test coverage --- skel/skel.go | 57 +++++++--- skel/skel_test.go | 232 ++++++++++++++++++++++++++++++++-------- testutils/bad_reader.go | 32 ++++++ 3 files changed, 263 insertions(+), 58 deletions(-) create mode 100644 testutils/bad_reader.go diff --git a/skel/skel.go b/skel/skel.go index 9cf03917..484ef374 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -18,6 +18,7 @@ package skel import ( "fmt" + "io" "io/ioutil" "log" "os" @@ -36,11 +37,14 @@ type CmdArgs struct { StdinData []byte } +type dispatcher struct { + Getenv func(string) string + Stdin io.Reader +} + type reqForCmdEntry map[string]bool -// PluginMain is the "main" for a plugin. It accepts -// two callback functions for add and del commands. -func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { +func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) { var cmd, contID, netns, ifName, args, path string vars := []struct { @@ -100,20 +104,21 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { argsMissing := false for _, v := range vars { - *v.val = os.Getenv(v.name) + *v.val = t.Getenv(v.name) if v.reqForCmd[cmd] && *v.val == "" { log.Printf("%v env variable missing", v.name) + // TODO: test this logging ^^^ and log to stderr instead of stdout argsMissing = true } } if argsMissing { - dieMsg("required env variables missing") + return "", nil, fmt.Errorf("required env variables missing") } - stdinData, err := ioutil.ReadAll(os.Stdin) + stdinData, err := ioutil.ReadAll(t.Stdin) if err != nil { - dieMsg("error reading from stdin: %v", err) + return "", nil, fmt.Errorf("error reading from stdin: %v", err) } cmdArgs := &CmdArgs{ @@ -124,6 +129,21 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { Path: path, StdinData: stdinData, } + return cmd, cmdArgs, nil +} + +func createTypedError(f string, args ...interface{}) *types.Error { + return &types.Error{ + Code: 100, + Msg: fmt.Sprintf(f, args...), + } +} + +func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Error { + cmd, cmdArgs, err := t.getCmdArgsFromEnv() + if err != nil { + return createTypedError(err.Error()) + } switch cmd { case "ADD": @@ -133,24 +153,31 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { err = cmdDel(cmdArgs) default: - dieMsg("unknown CNI_COMMAND: %v", cmd) + return createTypedError("unknown CNI_COMMAND: %v", cmd) } if err != nil { if e, ok := err.(*types.Error); ok { // don't wrap Error in Error - dieErr(e) + return e } - dieMsg(err.Error()) + return createTypedError(err.Error()) } + return nil } -func dieMsg(f string, args ...interface{}) { - e := &types.Error{ - Code: 100, - Msg: fmt.Sprintf(f, args...), +// PluginMain is the "main" for a plugin. It accepts +// two callback functions for add and del commands. +func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { + caller := dispatcher{ + Getenv: os.Getenv, + Stdin: os.Stdin, + } + + err := caller.pluginMain(cmdAdd, cmdDel) + if err != nil { + dieErr(err) } - dieErr(e) } func dieErr(e *types.Error) { diff --git a/skel/skel_test.go b/skel/skel_test.go index a52e0145..8a69d556 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -15,70 +15,216 @@ package skel import ( - "os" + "errors" + "io" + "strings" + "github.com/containernetworking/cni/pkg/types" + + "github.com/containernetworking/cni/pkg/testutils" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" ) -var _ = Describe("Skel", func() { +type fakeCmd struct { + CallCount int + Returns struct { + Error error + } + Received struct { + CmdArgs *CmdArgs + } +} + +func (c *fakeCmd) Func(args *CmdArgs) error { + c.CallCount++ + c.Received.CmdArgs = args + return c.Returns.Error +} + +var _ = Describe("dispatching to the correct callback", func() { var ( - fNoop = func(_ *CmdArgs) error { return nil } - // fErr = func(_ *CmdArgs) error { return errors.New("dummy") } - envVars = []struct { - name string - val string - }{ - {"CNI_CONTAINERID", "dummy"}, - {"CNI_NETNS", "dummy"}, - {"CNI_IFNAME", "dummy"}, - {"CNI_ARGS", "dummy"}, - {"CNI_PATH", "dummy"}, - } + environment map[string]string + stdin io.Reader + cmdAdd, cmdDel *fakeCmd + dispatch *dispatcher + expectedCmdArgs *CmdArgs ) - It("Must be possible to set the env vars", func() { - for _, v := range envVars { - err := os.Setenv(v.name, v.val) - Expect(err).NotTo(HaveOccurred()) + BeforeEach(func() { + environment = map[string]string{ + "CNI_COMMAND": "ADD", + "CNI_CONTAINERID": "some-container-id", + "CNI_NETNS": "/some/netns/path", + "CNI_IFNAME": "eth0", + "CNI_ARGS": "some;extra;args", + "CNI_PATH": "/some/cni/path", + } + stdin = strings.NewReader(`{ "some": "config" }`) + dispatch = &dispatcher{ + Getenv: func(key string) string { return environment[key] }, + Stdin: stdin, + } + cmdAdd = &fakeCmd{} + cmdDel = &fakeCmd{} + expectedCmdArgs = &CmdArgs{ + ContainerID: "some-container-id", + Netns: "/some/netns/path", + IfName: "eth0", + Args: "some;extra;args", + Path: "/some/cni/path", + StdinData: []byte(`{ "some": "config" }`), } }) - Context("When dummy environment variables are passed", func() { + var envVarChecker = func(envVar string, isRequired bool) { + delete(environment, envVar) - It("should not fail with ADD and noop callback", func() { - err := os.Setenv("CNI_COMMAND", "ADD") + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + if isRequired { + Expect(err).To(Equal(&types.Error{ + Code: 100, + Msg: "required env variables missing", + })) + } else { Expect(err).NotTo(HaveOccurred()) - PluginMain(fNoop, nil) + } + } + + Context("when the CNI_COMMAND is ADD", func() { + It("extracts env vars and stdin data and calls cmdAdd", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).NotTo(HaveOccurred()) + Expect(cmdAdd.CallCount).To(Equal(1)) + Expect(cmdDel.CallCount).To(Equal(0)) + Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) }) - // TODO: figure out howto mock printing and os.Exit() - // It("should fail with ADD and error callback", func() { - // err := os.Setenv("CNI_COMMAND", "ADD") - // Expect(err).NotTo(HaveOccurred()) - // PluginMain(fErr, nil) - // }) + It("does not call cmdDel", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) - It("should not fail with DEL and noop callback", func() { - err := os.Setenv("CNI_COMMAND", "DEL") Expect(err).NotTo(HaveOccurred()) - PluginMain(nil, fNoop) + Expect(cmdDel.CallCount).To(Equal(0)) }) - // TODO: figure out howto mock printing and os.Exit() - // It("should fail with DEL and error callback", func() { - // err := os.Setenv("CNI_COMMAND", "DEL") - // Expect(err).NotTo(HaveOccurred()) - // PluginMain(fErr, nil) - // }) + DescribeTable("required / optional env vars", envVarChecker, + // TODO: Entry("command", "CNI_COMMAND", true), + Entry("container id", "CNI_CONTAINER_ID", false), + Entry("net ns", "CNI_NETNS", true), + Entry("if name", "CNI_IFNAME", true), + Entry("args", "CNI_ARGS", false), + Entry("path", "CNI_PATH", true), + ) + }) - It("should not fail with DEL and no NETNS and noop callback", func() { - err := os.Setenv("CNI_COMMAND", "DEL") - Expect(err).NotTo(HaveOccurred()) - err = os.Unsetenv("CNI_NETNS") - Expect(err).NotTo(HaveOccurred()) - PluginMain(nil, fNoop) + Context("when the CNI_COMMAND is DEL", func() { + BeforeEach(func() { + environment["CNI_COMMAND"] = "DEL" }) + It("calls cmdDel with the env vars and stdin data", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).NotTo(HaveOccurred()) + Expect(cmdDel.CallCount).To(Equal(1)) + Expect(cmdDel.Received.CmdArgs).To(Equal(expectedCmdArgs)) + }) + + It("does not call cmdAdd", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).NotTo(HaveOccurred()) + Expect(cmdAdd.CallCount).To(Equal(0)) + }) + + DescribeTable("required / optional env vars", envVarChecker, + // TODO: Entry("command", "CNI_COMMAND", true), + Entry("container id", "CNI_CONTAINER_ID", false), + Entry("net ns", "CNI_NETNS", false), + Entry("if name", "CNI_IFNAME", true), + Entry("args", "CNI_ARGS", false), + Entry("path", "CNI_PATH", true), + ) + }) + + Context("when the CNI_COMMAND is unrecognized", func() { + BeforeEach(func() { + environment["CNI_COMMAND"] = "NOPE" + }) + + It("does not call any cmd callback", func() { + dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(cmdAdd.CallCount).To(Equal(0)) + Expect(cmdDel.CallCount).To(Equal(0)) + }) + + It("returns an error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).To(Equal(&types.Error{ + Code: 100, + Msg: "unknown CNI_COMMAND: NOPE", + })) + }) + }) + + Context("when stdin cannot be read", func() { + BeforeEach(func() { + dispatch.Stdin = &testutils.BadReader{} + }) + + It("does not call any cmd callback", func() { + dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(cmdAdd.CallCount).To(Equal(0)) + Expect(cmdDel.CallCount).To(Equal(0)) + }) + + It("wraps and returns the error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).To(Equal(&types.Error{ + Code: 100, + Msg: "error reading from stdin: banana", + })) + }) + }) + + Context("when the callback returns an error", func() { + Context("when it is a typed Error", func() { + BeforeEach(func() { + cmdAdd.Returns.Error = &types.Error{ + Code: 1234, + Msg: "insufficient something", + } + }) + + It("returns the error as-is", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).To(Equal(&types.Error{ + Code: 1234, + Msg: "insufficient something", + })) + }) + }) + + Context("when it is an unknown error", func() { + BeforeEach(func() { + cmdAdd.Returns.Error = errors.New("potato") + }) + + It("wraps and returns the error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).To(Equal(&types.Error{ + Code: 100, + Msg: "potato", + })) + }) + }) }) }) diff --git a/testutils/bad_reader.go b/testutils/bad_reader.go new file mode 100644 index 00000000..b3c0e97d --- /dev/null +++ b/testutils/bad_reader.go @@ -0,0 +1,32 @@ +// Copyright 2014 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import "errors" + +type BadReader struct { + Error error +} + +func (r *BadReader) Read(buffer []byte) (int, error) { + if r.Error != nil { + return 0, r.Error + } + return 0, errors.New("banana") +} + +func (r *BadReader) Close() error { + return nil +} From f2b6ec0375be3ea543385a08727fd7f347d2ae0b Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Wed, 13 Jul 2016 22:24:34 -0400 Subject: [PATCH 074/134] pkg/skel: missing env var log lines appear in stderr Previously, the log lines appeared in stdout before the JSON encoding of the error message. That would break JSON parsing of stdout. Instead, we use stderr for these unstructured logs, consistent with the CNI spec. --- skel/skel.go | 5 +++-- skel/skel_test.go | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/skel/skel.go b/skel/skel.go index 484ef374..bed405c3 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -40,6 +40,7 @@ type CmdArgs struct { type dispatcher struct { Getenv func(string) string Stdin io.Reader + Stderr io.Writer } type reqForCmdEntry map[string]bool @@ -106,8 +107,7 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) { for _, v := range vars { *v.val = t.Getenv(v.name) if v.reqForCmd[cmd] && *v.val == "" { - log.Printf("%v env variable missing", v.name) - // TODO: test this logging ^^^ and log to stderr instead of stdout + fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name) argsMissing = true } } @@ -172,6 +172,7 @@ func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { caller := dispatcher{ Getenv: os.Getenv, Stdin: os.Stdin, + Stderr: os.Stderr, } err := caller.pluginMain(cmdAdd, cmdDel) diff --git a/skel/skel_test.go b/skel/skel_test.go index 8a69d556..9974c9a5 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -15,6 +15,7 @@ package skel import ( + "bytes" "errors" "io" "strings" @@ -47,6 +48,7 @@ var _ = Describe("dispatching to the correct callback", func() { var ( environment map[string]string stdin io.Reader + stderr *bytes.Buffer cmdAdd, cmdDel *fakeCmd dispatch *dispatcher expectedCmdArgs *CmdArgs @@ -62,9 +64,11 @@ var _ = Describe("dispatching to the correct callback", func() { "CNI_PATH": "/some/cni/path", } stdin = strings.NewReader(`{ "some": "config" }`) + stderr = &bytes.Buffer{} dispatch = &dispatcher{ Getenv: func(key string) string { return environment[key] }, Stdin: stdin, + Stderr: stderr, } cmdAdd = &fakeCmd{} cmdDel = &fakeCmd{} @@ -87,6 +91,7 @@ var _ = Describe("dispatching to the correct callback", func() { Code: 100, Msg: "required env variables missing", })) + Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n")) } else { Expect(err).NotTo(HaveOccurred()) } @@ -117,6 +122,23 @@ var _ = Describe("dispatching to the correct callback", func() { Entry("args", "CNI_ARGS", false), Entry("path", "CNI_PATH", true), ) + + Context("when multiple required env vars are missing", func() { + BeforeEach(func() { + delete(environment, "CNI_NETNS") + delete(environment, "CNI_IFNAME") + delete(environment, "CNI_PATH") + }) + + It("reports that all of them are missing, not just the first", func() { + Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)).NotTo(Succeed()) + log := stderr.String() + Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n")) + Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n")) + Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n")) + + }) + }) }) Context("when the CNI_COMMAND is DEL", func() { From eda79f7645de017ad5e13e506ecbfbf7cc8ffd02 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Wed, 13 Jul 2016 22:54:22 -0400 Subject: [PATCH 075/134] pkg/skel: improve error message for missing CNI_COMMAND env var This makes the error message for missing CNI_COMMAND consistent with that of other required environment variables. --- skel/skel.go | 8 +++++--- skel/skel_test.go | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/skel/skel.go b/skel/skel.go index bed405c3..4325ec69 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -106,9 +106,11 @@ func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) { argsMissing := false for _, v := range vars { *v.val = t.Getenv(v.name) - if v.reqForCmd[cmd] && *v.val == "" { - fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name) - argsMissing = true + if *v.val == "" { + if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { + fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name) + argsMissing = true + } } } diff --git a/skel/skel_test.go b/skel/skel_test.go index 9974c9a5..39df2716 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -115,7 +115,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) DescribeTable("required / optional env vars", envVarChecker, - // TODO: Entry("command", "CNI_COMMAND", true), + Entry("command", "CNI_COMMAND", true), Entry("container id", "CNI_CONTAINER_ID", false), Entry("net ns", "CNI_NETNS", true), Entry("if name", "CNI_IFNAME", true), @@ -162,7 +162,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) DescribeTable("required / optional env vars", envVarChecker, - // TODO: Entry("command", "CNI_COMMAND", true), + Entry("command", "CNI_COMMAND", true), Entry("container id", "CNI_CONTAINER_ID", false), Entry("net ns", "CNI_NETNS", false), Entry("if name", "CNI_IFNAME", true), From 30c99d17cc87310eb3eb465822fc12a5e915e412 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Wed, 13 Jul 2016 23:50:54 -0400 Subject: [PATCH 076/134] pkg/skel: plugins now respond to VERSION command To support CNI spec versioning, plugins must be able to report version information to container runtimes. --- skel/skel.go | 20 ++++++++++++++------ skel/skel_test.go | 43 +++++++++++++++++++++++++++++++++++++++---- version/version.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 version/version.go diff --git a/skel/skel.go b/skel/skel.go index 4325ec69..b5d6ecf9 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -24,6 +24,7 @@ import ( "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) // CmdArgs captures all the arguments passed in to the plugin @@ -38,9 +39,11 @@ type CmdArgs struct { } type dispatcher struct { - Getenv func(string) string - Stdin io.Reader - Stderr io.Writer + Getenv func(string) string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + Versioner version.PluginVersioner } type reqForCmdEntry map[string]bool @@ -154,6 +157,9 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er case "DEL": err = cmdDel(cmdArgs) + case "VERSION": + err = t.Versioner.Encode(t.Stdout) + default: return createTypedError("unknown CNI_COMMAND: %v", cmd) } @@ -172,9 +178,11 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er // two callback functions for add and del commands. func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { caller := dispatcher{ - Getenv: os.Getenv, - Stdin: os.Stdin, - Stderr: os.Stderr, + Getenv: os.Getenv, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + Versioner: version.DefaultPluginVersioner, } err := caller.pluginMain(cmdAdd, cmdDel) diff --git a/skel/skel_test.go b/skel/skel_test.go index 39df2716..e6304f5e 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/testutils" . "github.com/onsi/ginkgo" @@ -48,7 +49,7 @@ var _ = Describe("dispatching to the correct callback", func() { var ( environment map[string]string stdin io.Reader - stderr *bytes.Buffer + stdout, stderr *bytes.Buffer cmdAdd, cmdDel *fakeCmd dispatch *dispatcher expectedCmdArgs *CmdArgs @@ -64,11 +65,15 @@ var _ = Describe("dispatching to the correct callback", func() { "CNI_PATH": "/some/cni/path", } stdin = strings.NewReader(`{ "some": "config" }`) + stdout = &bytes.Buffer{} stderr = &bytes.Buffer{} + versioner := &version.BasicVersioner{CNIVersion: "9.8.7"} dispatch = &dispatcher{ - Getenv: func(key string) string { return environment[key] }, - Stdin: stdin, - Stderr: stderr, + Getenv: func(key string) string { return environment[key] }, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + Versioner: versioner, } cmdAdd = &fakeCmd{} cmdDel = &fakeCmd{} @@ -171,6 +176,36 @@ var _ = Describe("dispatching to the correct callback", func() { ) }) + Context("when the CNI_COMMAND is VERSION", func() { + BeforeEach(func() { + environment["CNI_COMMAND"] = "VERSION" + }) + + It("prints the version to stdout", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout).To(MatchJSON(`{ "cniVersion": "9.8.7" }`)) + }) + + It("does not call cmdAdd or cmdDel", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + + Expect(err).NotTo(HaveOccurred()) + Expect(cmdAdd.CallCount).To(Equal(0)) + Expect(cmdDel.CallCount).To(Equal(0)) + }) + + DescribeTable("VERSION does not need the usual env vars", envVarChecker, + Entry("command", "CNI_COMMAND", true), + Entry("container id", "CNI_CONTAINER_ID", false), + Entry("net ns", "CNI_NETNS", false), + Entry("if name", "CNI_IFNAME", false), + Entry("args", "CNI_ARGS", false), + Entry("path", "CNI_PATH", false), + ) + }) + Context("when the CNI_COMMAND is unrecognized", func() { BeforeEach(func() { environment["CNI_COMMAND"] = "NOPE" diff --git a/version/version.go b/version/version.go new file mode 100644 index 00000000..2cb075fb --- /dev/null +++ b/version/version.go @@ -0,0 +1,42 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "encoding/json" + "io" +) + +// A PluginVersioner can encode information about its version +type PluginVersioner interface { + Encode(io.Writer) error +} + +// BasicVersioner is a PluginVersioner which reports a single cniVersion string +type BasicVersioner struct { + CNIVersion string `json:"cniVersion"` +} + +func (p *BasicVersioner) Encode(w io.Writer) error { + return json.NewEncoder(w).Encode(p) +} + +// Current reports the version of the CNI spec implemented by this library +func Current() string { + return "0.2.0" +} + +// DefaultPluginVersioner reports the Current library spec version as the cniVersion +var DefaultPluginVersioner = &BasicVersioner{CNIVersion: Current()} From 728071e7d10c2185620132d4853c64119617e2ee Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Thu, 14 Jul 2016 16:09:27 -0700 Subject: [PATCH 077/134] misc: fix up copyright dates --- skel/skel.go | 2 +- testutils/bad_reader.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skel/skel.go b/skel/skel.go index 4325ec69..d6f0f4cf 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -1,4 +1,4 @@ -// Copyright 2014 CNI authors +// Copyright 2014-2016 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/testutils/bad_reader.go b/testutils/bad_reader.go index b3c0e97d..ca06c5e9 100644 --- a/testutils/bad_reader.go +++ b/testutils/bad_reader.go @@ -1,4 +1,4 @@ -// Copyright 2014 CNI authors +// Copyright 2016 CNI authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 6496749565bf8b1e251eb001c4ee667726571419 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Thu, 14 Jul 2016 13:59:10 -0700 Subject: [PATCH 078/134] plugins: adds new no-op plugin that may be used as a test-double Plugin can be configured to record all inputs and to respond with arbitrary stdout or error message. Will support upcoming integration testing. --- testutils/bad_reader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/testutils/bad_reader.go b/testutils/bad_reader.go index ca06c5e9..f9d0aded 100644 --- a/testutils/bad_reader.go +++ b/testutils/bad_reader.go @@ -16,6 +16,7 @@ package testutils import "errors" +// BadReader is an io.Reader which always errors type BadReader struct { Error error } From c19b82efc21a0816ff50f97790a0fc0abce575b2 Mon Sep 17 00:00:00 2001 From: Minhan Xia Date: Tue, 3 May 2016 12:03:05 -0700 Subject: [PATCH 079/134] configure mac address based on assigned ip --- ipam/ipam.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ipam/ipam.go b/ipam/ipam.go index d9fbff74..7777e6f6 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -23,6 +23,17 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/vishvananda/netlink" + "net" + "strconv" + "strings" +) + +const ( + // private mac prefix safe to use + privateMACPrefix = "0a:58" + + // veth link dev type + vethLinkType = "veth" ) func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { @@ -45,6 +56,17 @@ func ConfigureIface(ifName string, res *types.Result) error { return fmt.Errorf("failed to set %q UP: %v", ifName, err) } + // only set hardware address to veth when using ipv4 + if link.Type() == vethLinkType && res.IP4 != nil { + hwAddr, err := generateHardwareAddr(res.IP4.IP.IP) + if err != nil { + return fmt.Errorf("failed to generate hardware addr: %v", err) + } + if err = netlink.LinkSetHardwareAddr(link, hwAddr); err != nil { + return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err) + } + } + // TODO(eyakubovich): IPv6 addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} if err = netlink.AddrAdd(link, addr); err != nil { @@ -66,3 +88,21 @@ func ConfigureIface(ifName string, res *types.Result) error { return nil } + +// generateHardwareAddr generates 48 bit virtual mac addresses based on the IP input. +func generateHardwareAddr(ip net.IP) (net.HardwareAddr, error) { + if ip.To4() == nil { + return nil, fmt.Errorf("generateHardwareAddr only support valid ipv4 address as input") + } + mac := privateMACPrefix + sections := strings.Split(ip.String(), ".") + for _, s := range sections { + i, _ := strconv.Atoi(s) + mac = mac + ":" + fmt.Sprintf("%02x", i) + } + hwAddr, err := net.ParseMAC(mac) + if err != nil { + return nil, fmt.Errorf("Failed to parse mac address %s generated based on ip %s due to: %v", mac, ip, err) + } + return hwAddr, nil +} From 75f1fa22602eb2b8d3c3153e52c958a489904baf Mon Sep 17 00:00:00 2001 From: Minhan Xia Date: Tue, 3 May 2016 15:47:28 -0700 Subject: [PATCH 080/134] add tests for generateHardwardAddr --- ipam/ipam_suite_test.go | 27 ++++++++++++++++++ ipam/ipam_test.go | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 ipam/ipam_suite_test.go create mode 100644 ipam/ipam_test.go diff --git a/ipam/ipam_suite_test.go b/ipam/ipam_suite_test.go new file mode 100644 index 00000000..e80c8675 --- /dev/null +++ b/ipam/ipam_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipam_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIpam(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ipam Suite") +} diff --git a/ipam/ipam_test.go b/ipam/ipam_test.go new file mode 100644 index 00000000..0cacd08f --- /dev/null +++ b/ipam/ipam_test.go @@ -0,0 +1,62 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipam + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "net" +) + +var _ = Describe("ipam utils", func() { + Context("Generate Hardware Addrress", func() { + It("generate hardware address based on ipv4 address", func() { + testCases := []struct { + ip net.IP + expectedMAC string + }{ + { + ip: net.ParseIP("10.0.0.2"), + expectedMAC: privateMACPrefix + ":0a:00:00:02", + }, + { + ip: net.ParseIP("10.250.0.244"), + expectedMAC: privateMACPrefix + ":0a:fa:00:f4", + }, + { + ip: net.ParseIP("172.17.0.2"), + expectedMAC: privateMACPrefix + ":ac:11:00:02", + }, + } + + for _, tc := range testCases { + mac, err := generateHardwareAddr(tc.ip) + Expect(err).NotTo(HaveOccurred()) + Expect(mac.String()).To(Equal(tc.expectedMAC)) + } + }) + + It("return error if input is not ipv4 address", func() { + testCases := []net.IP{ + net.ParseIP(""), + net.ParseIP("2001:db8:0:1:1:1:1:1"), + } + for _, tc := range testCases { + _, err := generateHardwareAddr(tc) + Expect(err.Error()).To(Equal("generateHardwareAddr only support valid ipv4 address as input")) + } + }) + }) +}) From 7ea07a6405f886a2f18f7497cb5416de1439e61d Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 21 Jun 2016 16:28:38 -0700 Subject: [PATCH 081/134] pkg/ipam{,test}: improve error handling and tests * _suite.go and _test.go file should be in the same package, using the _test package for that, which requires some fields and methods to be exported * Introduce error type for cleaner error handling * test adaptions for error type checking --- ipam/ipam.go | 39 +++++++++++++++++++++++++++++++-------- ipam/ipam_test.go | 23 +++++++++++++++-------- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/ipam/ipam.go b/ipam/ipam.go index 7777e6f6..74f937d7 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -30,7 +30,7 @@ import ( const ( // private mac prefix safe to use - privateMACPrefix = "0a:58" + PrivateMACPrefix = "0a:58" // veth link dev type vethLinkType = "veth" @@ -58,7 +58,7 @@ func ConfigureIface(ifName string, res *types.Result) error { // only set hardware address to veth when using ipv4 if link.Type() == vethLinkType && res.IP4 != nil { - hwAddr, err := generateHardwareAddr(res.IP4.IP.IP) + hwAddr, err := GenerateHardwareAddr4(res.IP4.IP.IP, PrivateMACPrefix) if err != nil { return fmt.Errorf("failed to generate hardware addr: %v", err) } @@ -89,20 +89,43 @@ func ConfigureIface(ifName string, res *types.Result) error { return nil } -// generateHardwareAddr generates 48 bit virtual mac addresses based on the IP input. -func generateHardwareAddr(ip net.IP) (net.HardwareAddr, error) { - if ip.To4() == nil { - return nil, fmt.Errorf("generateHardwareAddr only support valid ipv4 address as input") +type SupportIp4OnlyErr struct{ msg string } + +func (e SupportIp4OnlyErr) Error() string { return e.msg } + +type MacParseErr struct{ msg string } + +func (e MacParseErr) Error() string { return e.msg } + +type InvalidPrefixLengthErr struct{ msg string } + +func (e InvalidPrefixLengthErr) Error() string { return e.msg } + +// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. +func GenerateHardwareAddr4(ip net.IP, prefix string) (net.HardwareAddr, error) { + switch { + + case ip.To4() == nil: + return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"} + + case len(prefix) != len(PrivateMACPrefix): + return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf( + "Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)), + } } - mac := privateMACPrefix + + mac := prefix sections := strings.Split(ip.String(), ".") for _, s := range sections { i, _ := strconv.Atoi(s) mac = mac + ":" + fmt.Sprintf("%02x", i) } + hwAddr, err := net.ParseMAC(mac) if err != nil { - return nil, fmt.Errorf("Failed to parse mac address %s generated based on ip %s due to: %v", mac, ip, err) + return nil, MacParseErr{msg: fmt.Sprintf( + "Failed to parse mac address %q generated based on IP %q due to: %v", mac, ip, err), + } } return hwAddr, nil } diff --git a/ipam/ipam_test.go b/ipam/ipam_test.go index 0cacd08f..1390ccec 100644 --- a/ipam/ipam_test.go +++ b/ipam/ipam_test.go @@ -12,16 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ipam +package ipam_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "net" + + "github.com/containernetworking/cni/pkg/ipam" ) var _ = Describe("ipam utils", func() { - Context("Generate Hardware Addrress", func() { + Context("Generate Hardware Address", func() { It("generate hardware address based on ipv4 address", func() { testCases := []struct { ip net.IP @@ -29,20 +31,20 @@ var _ = Describe("ipam utils", func() { }{ { ip: net.ParseIP("10.0.0.2"), - expectedMAC: privateMACPrefix + ":0a:00:00:02", + expectedMAC: ipam.PrivateMACPrefix + ":0a:00:00:02", }, { ip: net.ParseIP("10.250.0.244"), - expectedMAC: privateMACPrefix + ":0a:fa:00:f4", + expectedMAC: ipam.PrivateMACPrefix + ":0a:fa:00:f4", }, { ip: net.ParseIP("172.17.0.2"), - expectedMAC: privateMACPrefix + ":ac:11:00:02", + expectedMAC: ipam.PrivateMACPrefix + ":ac:11:00:02", }, } for _, tc := range testCases { - mac, err := generateHardwareAddr(tc.ip) + mac, err := ipam.GenerateHardwareAddr4(tc.ip, ipam.PrivateMACPrefix) Expect(err).NotTo(HaveOccurred()) Expect(mac.String()).To(Equal(tc.expectedMAC)) } @@ -54,9 +56,14 @@ var _ = Describe("ipam utils", func() { net.ParseIP("2001:db8:0:1:1:1:1:1"), } for _, tc := range testCases { - _, err := generateHardwareAddr(tc) - Expect(err.Error()).To(Equal("generateHardwareAddr only support valid ipv4 address as input")) + _, err := ipam.GenerateHardwareAddr4(tc, ipam.PrivateMACPrefix) + Expect(err).To(BeAssignableToTypeOf(ipam.SupportIp4OnlyErr{})) } }) + + It("return error if prefix is invalid", func() { + _, err := ipam.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), "") + Expect(err).To(BeAssignableToTypeOf(ipam.InvalidPrefixLengthErr{})) + }) }) }) From c3e8563e776e3323729d4aae1d4d68bc39fd4a93 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 28 Jun 2016 12:51:58 -0700 Subject: [PATCH 082/134] pkg/utils/hwaddr: migrate code from IPAM pkg --- ipam/ipam.go | 50 +------------- utils/hwaddr/hwaddr.go | 68 +++++++++++++++++++ utils/hwaddr/hwaddr_suite_test.go | 27 ++++++++ .../hwaddr/hwaddr_test.go | 27 ++++---- 4 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 utils/hwaddr/hwaddr.go create mode 100644 utils/hwaddr/hwaddr_suite_test.go rename ipam/ipam_test.go => utils/hwaddr/hwaddr_test.go (68%) diff --git a/ipam/ipam.go b/ipam/ipam.go index 74f937d7..62bf15d5 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -21,17 +21,12 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" - "net" - "strconv" - "strings" ) const ( - // private mac prefix safe to use - PrivateMACPrefix = "0a:58" - // veth link dev type vethLinkType = "veth" ) @@ -58,7 +53,7 @@ func ConfigureIface(ifName string, res *types.Result) error { // only set hardware address to veth when using ipv4 if link.Type() == vethLinkType && res.IP4 != nil { - hwAddr, err := GenerateHardwareAddr4(res.IP4.IP.IP, PrivateMACPrefix) + hwAddr, err := hwaddr.GenerateHardwareAddr4(res.IP4.IP.IP, hwaddr.PrivateMACPrefix) if err != nil { return fmt.Errorf("failed to generate hardware addr: %v", err) } @@ -88,44 +83,3 @@ func ConfigureIface(ifName string, res *types.Result) error { return nil } - -type SupportIp4OnlyErr struct{ msg string } - -func (e SupportIp4OnlyErr) Error() string { return e.msg } - -type MacParseErr struct{ msg string } - -func (e MacParseErr) Error() string { return e.msg } - -type InvalidPrefixLengthErr struct{ msg string } - -func (e InvalidPrefixLengthErr) Error() string { return e.msg } - -// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. -func GenerateHardwareAddr4(ip net.IP, prefix string) (net.HardwareAddr, error) { - switch { - - case ip.To4() == nil: - return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"} - - case len(prefix) != len(PrivateMACPrefix): - return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf( - "Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)), - } - } - - mac := prefix - sections := strings.Split(ip.String(), ".") - for _, s := range sections { - i, _ := strconv.Atoi(s) - mac = mac + ":" + fmt.Sprintf("%02x", i) - } - - hwAddr, err := net.ParseMAC(mac) - if err != nil { - return nil, MacParseErr{msg: fmt.Sprintf( - "Failed to parse mac address %q generated based on IP %q due to: %v", mac, ip, err), - } - } - return hwAddr, nil -} diff --git a/utils/hwaddr/hwaddr.go b/utils/hwaddr/hwaddr.go new file mode 100644 index 00000000..f03b7aca --- /dev/null +++ b/utils/hwaddr/hwaddr.go @@ -0,0 +1,68 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hwaddr + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +const ( + // private mac prefix safe to use + PrivateMACPrefix = "0a:58" +) + +type SupportIp4OnlyErr struct{ msg string } + +func (e SupportIp4OnlyErr) Error() string { return e.msg } + +type MacParseErr struct{ msg string } + +func (e MacParseErr) Error() string { return e.msg } + +type InvalidPrefixLengthErr struct{ msg string } + +func (e InvalidPrefixLengthErr) Error() string { return e.msg } + +// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. +func GenerateHardwareAddr4(ip net.IP, prefix string) (net.HardwareAddr, error) { + switch { + + case ip.To4() == nil: + return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"} + + case len(prefix) != len(PrivateMACPrefix): + return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf( + "Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)), + } + } + + mac := prefix + sections := strings.Split(ip.String(), ".") + for _, s := range sections { + i, _ := strconv.Atoi(s) + mac += fmt.Sprintf(":%02x", i) + } + + hwAddr, err := net.ParseMAC(mac) + if err != nil { + return nil, MacParseErr{msg: fmt.Sprintf( + "Failed to parse mac address %q generated based on IP %q due to: %v", mac, ip, err), + } + } + return hwAddr, nil +} diff --git a/utils/hwaddr/hwaddr_suite_test.go b/utils/hwaddr/hwaddr_suite_test.go new file mode 100644 index 00000000..e3bbfe97 --- /dev/null +++ b/utils/hwaddr/hwaddr_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hwaddr_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestHwaddr(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Hwaddr Suite") +} diff --git a/ipam/ipam_test.go b/utils/hwaddr/hwaddr_test.go similarity index 68% rename from ipam/ipam_test.go rename to utils/hwaddr/hwaddr_test.go index 1390ccec..8a201a25 100644 --- a/ipam/ipam_test.go +++ b/utils/hwaddr/hwaddr_test.go @@ -12,17 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ipam_test +package hwaddr_test import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "net" - "github.com/containernetworking/cni/pkg/ipam" + "github.com/containernetworking/cni/pkg/utils/hwaddr" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) -var _ = Describe("ipam utils", func() { +var _ = Describe("Hwaddr", func() { Context("Generate Hardware Address", func() { It("generate hardware address based on ipv4 address", func() { testCases := []struct { @@ -31,20 +32,20 @@ var _ = Describe("ipam utils", func() { }{ { ip: net.ParseIP("10.0.0.2"), - expectedMAC: ipam.PrivateMACPrefix + ":0a:00:00:02", + expectedMAC: hwaddr.PrivateMACPrefix + ":0a:00:00:02", }, { ip: net.ParseIP("10.250.0.244"), - expectedMAC: ipam.PrivateMACPrefix + ":0a:fa:00:f4", + expectedMAC: hwaddr.PrivateMACPrefix + ":0a:fa:00:f4", }, { ip: net.ParseIP("172.17.0.2"), - expectedMAC: ipam.PrivateMACPrefix + ":ac:11:00:02", + expectedMAC: hwaddr.PrivateMACPrefix + ":ac:11:00:02", }, } for _, tc := range testCases { - mac, err := ipam.GenerateHardwareAddr4(tc.ip, ipam.PrivateMACPrefix) + mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix) Expect(err).NotTo(HaveOccurred()) Expect(mac.String()).To(Equal(tc.expectedMAC)) } @@ -56,14 +57,14 @@ var _ = Describe("ipam utils", func() { net.ParseIP("2001:db8:0:1:1:1:1:1"), } for _, tc := range testCases { - _, err := ipam.GenerateHardwareAddr4(tc, ipam.PrivateMACPrefix) - Expect(err).To(BeAssignableToTypeOf(ipam.SupportIp4OnlyErr{})) + _, err := hwaddr.GenerateHardwareAddr4(tc, hwaddr.PrivateMACPrefix) + Expect(err).To(BeAssignableToTypeOf(hwaddr.SupportIp4OnlyErr{})) } }) It("return error if prefix is invalid", func() { - _, err := ipam.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), "") - Expect(err).To(BeAssignableToTypeOf(ipam.InvalidPrefixLengthErr{})) + _, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), "") + Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{})) }) }) }) From b6137de9930a6519dae4c5100afabdc6d4374676 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 28 Jun 2016 17:46:17 -0700 Subject: [PATCH 083/134] plugins: set MAC addresses based on IP This will give deterministic MAC addresses for all interfaces CNI creates and manages the IP for: * bridge: container veth and host bridge * macvlan: container veth * ptp: container veth and host veth --- ip/link.go | 17 +++++++++++++++++ ipam/ipam.go | 17 ----------------- utils/hwaddr/hwaddr.go | 31 +++++++++++++------------------ utils/hwaddr/hwaddr_test.go | 12 ++++++------ 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/ip/link.go b/ip/link.go index 1b785672..6d9d1cc2 100644 --- a/ip/link.go +++ b/ip/link.go @@ -21,6 +21,7 @@ import ( "os" "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" ) @@ -151,3 +152,19 @@ func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { return addrs[0].IPNet, nil } + +func SetHWAddrByIP(link netlink.Link, ip4 net.IP, ip6 net.IP) error { + if ip4 != nil { + hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix) + if err != nil { + return fmt.Errorf("failed to generate hardware addr: %v", err) + } + if err = netlink.LinkSetHardwareAddr(link, hwAddr); err != nil { + return fmt.Errorf("failed to add hardware addr to %q: %v", link.Attrs().Name, err) + } + } + + // TODO: IPv6 + + return nil +} diff --git a/ipam/ipam.go b/ipam/ipam.go index 62bf15d5..d9fbff74 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -21,16 +21,10 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" ) -const ( - // veth link dev type - vethLinkType = "veth" -) - func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { return invoke.DelegateAdd(plugin, netconf) } @@ -51,17 +45,6 @@ func ConfigureIface(ifName string, res *types.Result) error { return fmt.Errorf("failed to set %q UP: %v", ifName, err) } - // only set hardware address to veth when using ipv4 - if link.Type() == vethLinkType && res.IP4 != nil { - hwAddr, err := hwaddr.GenerateHardwareAddr4(res.IP4.IP.IP, hwaddr.PrivateMACPrefix) - if err != nil { - return fmt.Errorf("failed to generate hardware addr: %v", err) - } - if err = netlink.LinkSetHardwareAddr(link, hwAddr); err != nil { - return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err) - } - } - // TODO(eyakubovich): IPv6 addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} if err = netlink.AddrAdd(link, addr); err != nil { diff --git a/utils/hwaddr/hwaddr.go b/utils/hwaddr/hwaddr.go index f03b7aca..aaf3b8a0 100644 --- a/utils/hwaddr/hwaddr.go +++ b/utils/hwaddr/hwaddr.go @@ -17,13 +17,16 @@ package hwaddr import ( "fmt" "net" - "strconv" - "strings" ) const ( + ipRelevantByteLen = 4 + PrivateMACPrefixString = "0a:58" +) + +var ( // private mac prefix safe to use - PrivateMACPrefix = "0a:58" + PrivateMACPrefix = []byte{0x0a, 0x58} ) type SupportIp4OnlyErr struct{ msg string } @@ -39,7 +42,7 @@ type InvalidPrefixLengthErr struct{ msg string } func (e InvalidPrefixLengthErr) Error() string { return e.msg } // GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. -func GenerateHardwareAddr4(ip net.IP, prefix string) (net.HardwareAddr, error) { +func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) { switch { case ip.To4() == nil: @@ -51,18 +54,10 @@ func GenerateHardwareAddr4(ip net.IP, prefix string) (net.HardwareAddr, error) { } } - mac := prefix - sections := strings.Split(ip.String(), ".") - for _, s := range sections { - i, _ := strconv.Atoi(s) - mac += fmt.Sprintf(":%02x", i) - } - - hwAddr, err := net.ParseMAC(mac) - if err != nil { - return nil, MacParseErr{msg: fmt.Sprintf( - "Failed to parse mac address %q generated based on IP %q due to: %v", mac, ip, err), - } - } - return hwAddr, nil + ipByteLen := len(ip) + return (net.HardwareAddr)( + append( + prefix, + ip[ipByteLen-ipRelevantByteLen:ipByteLen]...), + ), nil } diff --git a/utils/hwaddr/hwaddr_test.go b/utils/hwaddr/hwaddr_test.go index 8a201a25..51c4e498 100644 --- a/utils/hwaddr/hwaddr_test.go +++ b/utils/hwaddr/hwaddr_test.go @@ -28,26 +28,26 @@ var _ = Describe("Hwaddr", func() { It("generate hardware address based on ipv4 address", func() { testCases := []struct { ip net.IP - expectedMAC string + expectedMAC net.HardwareAddr }{ { ip: net.ParseIP("10.0.0.2"), - expectedMAC: hwaddr.PrivateMACPrefix + ":0a:00:00:02", + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0x00, 0x00, 0x02)), }, { ip: net.ParseIP("10.250.0.244"), - expectedMAC: hwaddr.PrivateMACPrefix + ":0a:fa:00:f4", + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0xfa, 0x00, 0xf4)), }, { ip: net.ParseIP("172.17.0.2"), - expectedMAC: hwaddr.PrivateMACPrefix + ":ac:11:00:02", + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)), }, } for _, tc := range testCases { mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix) Expect(err).NotTo(HaveOccurred()) - Expect(mac.String()).To(Equal(tc.expectedMAC)) + Expect(mac).To(Equal(tc.expectedMAC)) } }) @@ -63,7 +63,7 @@ var _ = Describe("Hwaddr", func() { }) It("return error if prefix is invalid", func() { - _, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), "") + _, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), []byte{0x58}) Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{})) }) }) From 1a04d9abba8d7fd126d89b601e69a87c16e68188 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Tue, 28 Jun 2016 19:52:08 -0700 Subject: [PATCH 084/134] pkg/ip tests: cover SetupVeth and DelLinkByName* --- ip/ip_suite_test.go | 27 +++++++++ ip/link_test.go | 132 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 ip/ip_suite_test.go create mode 100644 ip/link_test.go diff --git a/ip/ip_suite_test.go b/ip/ip_suite_test.go new file mode 100644 index 00000000..3fdd57e4 --- /dev/null +++ b/ip/ip_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ip Suite") +} diff --git a/ip/link_test.go b/ip/link_test.go new file mode 100644 index 00000000..cdf861a6 --- /dev/null +++ b/ip/link_test.go @@ -0,0 +1,132 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip_test + +import ( + "fmt" + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/ns" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +var _ = Describe("Link", func() { + const ( + ifaceFormatString string = "i%d" + mtu int = 1400 + ) + var ( + hostNetNS ns.NetNS + containerNetNS ns.NetNS + ifaceCounter int = 0 + hostVethName string + containerVethName string + ) + + BeforeEach(func() { + var err error + + hostNetNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + containerNetNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + hostVeth, containerVeth, err := ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS) + if err != nil { + return err + } + Expect(err).NotTo(HaveOccurred()) + + hostVethName = hostVeth.Attrs().Name + containerVethName = containerVeth.Attrs().Name + + return nil + }) + }) + + AfterEach(func() { + Expect(containerNetNS.Close()).To(Succeed()) + Expect(hostNetNS.Close()).To(Succeed()) + ifaceCounter++ + }) + + It("SetupVeth must put the veth endpoints into the separate namespaces", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(containerVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + + _ = hostNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(hostVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + }) + + It("DelLinkByName must delete the veth endpoints", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // this will delete the host endpoint too + err := ip.DelLinkByName(containerVethName) + Expect(err).NotTo(HaveOccurred()) + + _, err = netlink.LinkByName(containerVethName) + Expect(err).To(HaveOccurred()) + + return nil + }) + + _ = hostNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(hostVethName) + Expect(err).To(HaveOccurred()) + + return nil + }) + }) + + It("DelLinkByNameAddr must throw an error for configured interfaces", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // this will delete the host endpoint too + addr, err := ip.DelLinkByNameAddr(containerVethName, nl.FAMILY_V4) + Expect(err).To(HaveOccurred()) + + var ipNetNil *net.IPNet + Expect(addr).To(Equal(ipNetNil)) + return nil + }) + }) +}) From c317888ac5285daa431c2a06b3c97c7f82dceaea Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 29 Jun 2016 12:13:05 -0700 Subject: [PATCH 085/134] pkg/ip: use iface name in SetHWAddrByIP --- ip/link.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/ip/link.go b/ip/link.go index 6d9d1cc2..c9662d98 100644 --- a/ip/link.go +++ b/ip/link.go @@ -153,18 +153,29 @@ func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { return addrs[0].IPNet, nil } -func SetHWAddrByIP(link netlink.Link, ip4 net.IP, ip6 net.IP) error { - if ip4 != nil { - hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix) - if err != nil { - return fmt.Errorf("failed to generate hardware addr: %v", err) - } - if err = netlink.LinkSetHardwareAddr(link, hwAddr); err != nil { - return fmt.Errorf("failed to add hardware addr to %q: %v", link.Attrs().Name, err) - } +func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) } - // TODO: IPv6 + switch { + case ip4 == nil && ip6 == nil: + return fmt.Errorf("neither ip4 or ip6 specified") + + case ip4 != nil: + { + hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix) + if err != nil { + return fmt.Errorf("failed to generate hardware addr: %v", err) + } + if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil { + return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err) + } + } + case ip6 != nil: + // TODO: IPv6 + } return nil } From 00f27577d5ae3f3c8d6969451c26a8e610c79719 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Wed, 29 Jun 2016 12:13:37 -0700 Subject: [PATCH 086/134] pkg/ip link_test: ensure SetHWAddrByIP has an effect --- ip/link_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/ip/link_test.go b/ip/link_test.go index cdf861a6..5c139d98 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -28,10 +28,17 @@ import ( "github.com/vishvananda/netlink/nl" ) +func getHwAddr(linkname string) string { + veth, err := netlink.LinkByName(linkname) + Expect(err).NotTo(HaveOccurred()) + return fmt.Sprintf("%s", veth.Attrs().HardwareAddr) +} + var _ = Describe("Link", func() { const ( ifaceFormatString string = "i%d" mtu int = 1400 + ip4onehwaddr = "0a:58:01:01:01:01" ) var ( hostNetNS ns.NetNS @@ -39,6 +46,9 @@ var _ = Describe("Link", func() { ifaceCounter int = 0 hostVethName string containerVethName string + + ip4one = net.ParseIP("1.1.1.1") + ip4two = net.ParseIP("1.1.1.2") ) BeforeEach(func() { @@ -129,4 +139,41 @@ var _ = Describe("Link", func() { return nil }) }) + + It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() { + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + var err error + hwaddrBefore := getHwAddr(containerVethName) + + err = ip.SetHWAddrByIP(containerVethName, ip4one, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter1 := getHwAddr(containerVethName) + + Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1)) + Expect(hwaddrAfter1).To(Equal(ip4onehwaddr)) + + return nil + }) + }) + + It("SetHWAddrByIP must be injective", func() { + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ip.SetHWAddrByIP(containerVethName, ip4one, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter1 := getHwAddr(containerVethName) + + err = ip.SetHWAddrByIP(containerVethName, ip4two, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter2 := getHwAddr(containerVethName) + + Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2)) + return nil + }) + }) }) From 22a0ddc9060350f422870db345957455d5f7a525 Mon Sep 17 00:00:00 2001 From: Stefan Junker Date: Mon, 1 Aug 2016 16:50:40 -0700 Subject: [PATCH 087/134] pkg/utils/hwaddr tests: cover v4 in v6 addr --- utils/hwaddr/hwaddr_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/utils/hwaddr/hwaddr_test.go b/utils/hwaddr/hwaddr_test.go index 51c4e498..b77ccd89 100644 --- a/utils/hwaddr/hwaddr_test.go +++ b/utils/hwaddr/hwaddr_test.go @@ -42,6 +42,10 @@ var _ = Describe("Hwaddr", func() { ip: net.ParseIP("172.17.0.2"), expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)), }, + { + ip: net.IPv4(byte(172), byte(17), byte(0), byte(2)), + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)), + }, } for _, tc := range testCases { From 3c1d4581c46aad49bc1c3fc1ead6f15b9806559a Mon Sep 17 00:00:00 2001 From: Prateek Gogia Date: Sun, 7 Aug 2016 19:24:56 +0000 Subject: [PATCH 088/134] pkg/ip: Return correct error if container name provided exists, and test cases If interface name for a container provided by a user is already present, Veth creation fails with incorrect error. If os.IsExist error is returned by makeVethPair: * Check for peer name, if exists generate another random peer name, * else, IsExist error is due to container interface present, return error. Fixes #155 --- ip/link.go | 13 +++++++- ip/link_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/ip/link.go b/ip/link.go index c9662d98..1cab50cd 100644 --- a/ip/link.go +++ b/ip/link.go @@ -41,6 +41,13 @@ func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { return veth, nil } +func peerExists(name string) bool { + if _, err := netlink.LinkByName(name); err != nil { + return false + } + return true +} + func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) { for i := 0; i < 10; i++ { peerName, err = RandomVethName() @@ -54,7 +61,11 @@ func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err err return case os.IsExist(err): - continue + if peerExists(peerName) { + continue + } + err = fmt.Errorf("container veth name provided (%v) already exists", name) + return default: err = fmt.Errorf("failed to make veth pair: %v", err) diff --git a/ip/link_test.go b/ip/link_test.go index 5c139d98..d4eff9b0 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -15,6 +15,8 @@ package ip_test import ( + "bytes" + "crypto/rand" "fmt" "net" @@ -47,8 +49,9 @@ var _ = Describe("Link", func() { hostVethName string containerVethName string - ip4one = net.ParseIP("1.1.1.1") - ip4two = net.ParseIP("1.1.1.2") + ip4one = net.ParseIP("1.1.1.1") + ip4two = net.ParseIP("1.1.1.2") + originalRandReader = rand.Reader ) BeforeEach(func() { @@ -60,6 +63,10 @@ var _ = Describe("Link", func() { containerNetNS, err = ns.NewNS() Expect(err).NotTo(HaveOccurred()) + fakeBytes := make([]byte, 20) + //to be reset in AfterEach block + rand.Reader = bytes.NewReader(fakeBytes) + _ = containerNetNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -80,6 +87,7 @@ var _ = Describe("Link", func() { Expect(containerNetNS.Close()).To(Succeed()) Expect(hostNetNS.Close()).To(Succeed()) ifaceCounter++ + rand.Reader = originalRandReader }) It("SetupVeth must put the veth endpoints into the separate namespaces", func() { @@ -102,6 +110,74 @@ var _ = Describe("Link", func() { }) }) + Context("when container already has an interface with the same name", func() { + It("returns useful error", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS) + Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName))) + + return nil + }) + }) + }) + + Context("when there is no name available for the host-side", func() { + BeforeEach(func() { + //adding different interface to container ns + containerVethName += "0" + }) + It("returns useful error", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS) + Expect(err.Error()).To(Equal("failed to move veth to host netns: file exists")) + + return nil + }) + }) + }) + + Context("when there is no name conflict for the host or container interfaces", func() { + BeforeEach(func() { + //adding different interface to container and host ns + containerVethName += "0" + rand.Reader = originalRandReader + }) + It("successfully creates the second veth pair", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS) + Expect(err).NotTo(HaveOccurred()) + hostVethName = hostVeth.Attrs().Name + return nil + }) + + //verify veths are in different namespaces + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(containerVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + + _ = hostNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(hostVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + }) + + }) + It("DelLinkByName must delete the veth endpoints", func() { _ = containerNetNS.Do(func(ns.NetNS) error { defer GinkgoRecover() From 0e675fa14fc062a17b66db3dc093b40731c50b57 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 30 Aug 2016 23:52:47 -0400 Subject: [PATCH 089/134] invoke: backfill tests for plugin execution --- invoke/exec.go | 17 ++++- invoke/exec_test.go | 123 ++++++++++++++++++++++++++++++++++++ invoke/invoke_suite_test.go | 18 ++++++ 3 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 invoke/exec_test.go diff --git a/invoke/exec.go b/invoke/exec.go index a85eede6..d7e38f21 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "os" "os/exec" @@ -57,15 +58,25 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) er } func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) { + return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) +} + +var defaultRawExec = &RawExec{Stderr: os.Stderr} + +type RawExec struct { + Stderr io.Writer +} + +func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { stdout := &bytes.Buffer{} c := exec.Cmd{ - Env: args.AsEnv(), + Env: environ, Path: pluginPath, Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(netconf), + Stdin: bytes.NewBuffer(stdinData), Stdout: stdout, - Stderr: os.Stderr, + Stderr: e.Stderr, } if err := c.Run(); err != nil { return nil, pluginErr(err, stdout.Bytes()) diff --git a/invoke/exec_test.go b/invoke/exec_test.go new file mode 100644 index 00000000..7df60a11 --- /dev/null +++ b/invoke/exec_test.go @@ -0,0 +1,123 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke_test + +import ( + "bytes" + "io/ioutil" + "os" + + "github.com/containernetworking/cni/pkg/invoke" + + noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RawExec", func() { + var ( + debugFileName string + debug *noop_debug.Debug + environ []string + stdin []byte + execer *invoke.RawExec + ) + + const reportResult = `{ "some": "result" }` + + BeforeEach(func() { + debugFile, err := ioutil.TempFile("", "cni_debug") + Expect(err).NotTo(HaveOccurred()) + Expect(debugFile.Close()).To(Succeed()) + debugFileName = debugFile.Name() + + debug = &noop_debug.Debug{ + ReportResult: reportResult, + ReportStderr: "some stderr message", + } + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + + environ = []string{ + "CNI_COMMAND=ADD", + "CNI_CONTAINERID=some-container-id", + "CNI_ARGS=DEBUG=" + debugFileName, + "CNI_NETNS=/some/netns/path", + "CNI_PATH=/some/bin/path", + "CNI_IFNAME=some-eth0", + } + stdin = []byte(`{"some":"stdin-json"}`) + execer = &invoke.RawExec{} + }) + + AfterEach(func() { + Expect(os.Remove(debugFileName)).To(Succeed()) + }) + + It("runs the plugin with the given stdin and environment", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + debug, err := noop_debug.ReadDebug(debugFileName) + Expect(err).NotTo(HaveOccurred()) + Expect(debug.Command).To(Equal("ADD")) + Expect(debug.CmdArgs.StdinData).To(Equal(stdin)) + Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path")) + }) + + It("returns the resulting stdout as bytes", func() { + resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + Expect(resultBytes).To(BeEquivalentTo(reportResult)) + }) + + Context("when the Stderr writer is set", func() { + var stderrBuffer *bytes.Buffer + + BeforeEach(func() { + stderrBuffer = &bytes.Buffer{} + execer.Stderr = stderrBuffer + }) + + It("forwards any stderr bytes to the Stderr writer", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + Expect(stderrBuffer.String()).To(Equal("some stderr message")) + }) + }) + + Context("when the plugin errors", func() { + BeforeEach(func() { + debug.ReportError = "banana" + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + }) + + It("wraps and returns the error", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("banana")) + }) + }) + + Context("when the system is unable to execute the plugin", func() { + It("returns the error", func() { + _, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path"))) + }) + }) +}) diff --git a/invoke/invoke_suite_test.go b/invoke/invoke_suite_test.go index 72eb837f..7285878a 100644 --- a/invoke/invoke_suite_test.go +++ b/invoke/invoke_suite_test.go @@ -17,6 +17,7 @@ package invoke_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" "testing" ) @@ -25,3 +26,20 @@ func TestInvoke(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Invoke Suite") } + +const packagePath = "github.com/containernetworking/cni/plugins/test/noop" + +var pathToPlugin string + +var _ = SynchronizedBeforeSuite(func() []byte { + var err error + pathToPlugin, err = gexec.Build(packagePath) + Expect(err).NotTo(HaveOccurred()) + return []byte(pathToPlugin) +}, func(crossNodeData []byte) { + pathToPlugin = string(crossNodeData) +}) + +var _ = SynchronizedAfterSuite(func() {}, func() { + gexec.CleanupBuildArtifacts() +}) From 31ef82276a4579f547a88c6a8a5c5a792abcfbfc Mon Sep 17 00:00:00 2001 From: Tom Denham Date: Mon, 29 Aug 2016 14:10:36 -0700 Subject: [PATCH 090/134] pkg/ip: Ensure that SetupVeth returns correct hostVeth The veth is moved from the container NS to the host NS. This is handled by the code that sets the link to UP but the wrong hostVeth is returned to the calling code. --- ip/link.go | 2 +- ip/link_test.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ip/link.go b/ip/link.go index 1cab50cd..43b37390 100644 --- a/ip/link.go +++ b/ip/link.go @@ -117,7 +117,7 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVet } err = hostNS.Do(func(_ ns.NetNS) error { - hostVeth, err := netlink.LinkByName(hostVethName) + hostVeth, err = netlink.LinkByName(hostVethName) if err != nil { return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err) } diff --git a/ip/link_test.go b/ip/link_test.go index d4eff9b0..3df9ab8f 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -46,6 +46,8 @@ var _ = Describe("Link", func() { hostNetNS ns.NetNS containerNetNS ns.NetNS ifaceCounter int = 0 + hostVeth netlink.Link + containerVeth netlink.Link hostVethName string containerVethName string @@ -70,7 +72,7 @@ var _ = Describe("Link", func() { _ = containerNetNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - hostVeth, containerVeth, err := ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS) + hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS) if err != nil { return err } @@ -94,8 +96,9 @@ var _ = Describe("Link", func() { _ = containerNetNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := netlink.LinkByName(containerVethName) + containerVethFromName, err := netlink.LinkByName(containerVethName) Expect(err).NotTo(HaveOccurred()) + Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Attrs().Index)) return nil }) @@ -103,8 +106,9 @@ var _ = Describe("Link", func() { _ = hostNetNS.Do(func(ns.NetNS) error { defer GinkgoRecover() - _, err := netlink.LinkByName(hostVethName) + hostVethFromName, err := netlink.LinkByName(hostVethName) Expect(err).NotTo(HaveOccurred()) + Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Attrs().Index)) return nil }) From 1ce5e54e998046687f0c1056cefd720126e40e23 Mon Sep 17 00:00:00 2001 From: Avinash Sridharan Date: Wed, 24 Aug 2016 23:49:50 +0000 Subject: [PATCH 091/134] pkg/ns: fixed the check for network namespace path. The expectation on older kernels (< 3.19) was to have the network namespace always be a directory. This is not true if the network namespace is bind mounted to a file, and will make the plugin fail erroneously in such cases. The fix is to remove this assumption completely and just do a basic check on the file system types being returned. Fixes #288 --- ns/ns.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/ns/ns.go b/ns/ns.go index e29f712c..5c531c04 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -20,7 +20,6 @@ import ( "os" "path" "runtime" - "strings" "sync" "syscall" @@ -101,19 +100,7 @@ func IsNSorErr(nspath string) error { } switch stat.Type { - case PROCFS_MAGIC: - // Kernel < 3.19 - - validPathContent := "ns/" - validName := strings.Contains(nspath, validPathContent) - if !validName { - return NSPathNotNSErr{msg: fmt.Sprintf("path %q doesn't contain %q", nspath, validPathContent)} - } - - return nil - case NSFS_MAGIC: - // Kernel >= 3.19 - + case PROCFS_MAGIC, NSFS_MAGIC: return nil default: return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} From b1e254d901642ea6191d34b44bb80538a2b25020 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 21 Aug 2016 23:48:04 -0700 Subject: [PATCH 092/134] versioning: plugins report a list of supported versions Further progress on versioning support (Issue #266). Bump CNI spec version to 0.3.0 --- invoke/exec.go | 10 +++++ skel/skel.go | 26 ++++++------- skel/skel_test.go | 45 ++++++++++++---------- version/version.go | 62 ++++++++++++++++++++++++------ version/version_suite_test.go | 27 +++++++++++++ version/version_test.go | 72 +++++++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 46 deletions(-) create mode 100644 version/version_suite_test.go create mode 100644 version/version_test.go diff --git a/invoke/exec.go b/invoke/exec.go index d7e38f21..3d32ab7c 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -23,6 +23,7 @@ import ( "os/exec" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) func pluginErr(err error, output []byte) error { @@ -57,6 +58,15 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) er return err } +func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) { + stdoutBytes, err := execPlugin(pluginPath, nil, &Args{Command: "VERSION"}) + if err != nil { + return nil, err + } + + return version.Decode(stdoutBytes) +} + func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) { return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) } diff --git a/skel/skel.go b/skel/skel.go index 19ddf249..de64d7dd 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -39,11 +39,10 @@ type CmdArgs struct { } type dispatcher struct { - Getenv func(string) string - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - Versioner version.PluginVersioner + Getenv func(string) string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer } type reqForCmdEntry map[string]bool @@ -144,7 +143,7 @@ func createTypedError(f string, args ...interface{}) *types.Error { } } -func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Error { +func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { cmd, cmdArgs, err := t.getCmdArgsFromEnv() if err != nil { return createTypedError(err.Error()) @@ -158,7 +157,7 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er err = cmdDel(cmdArgs) case "VERSION": - err = t.Versioner.Encode(t.Stdout) + err = versionInfo.Encode(t.Stdout) default: return createTypedError("unknown CNI_COMMAND: %v", cmd) @@ -176,16 +175,15 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er // PluginMain is the "main" for a plugin. It accepts // two callback functions for add and del commands. -func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { +func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) { caller := dispatcher{ - Getenv: os.Getenv, - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - Versioner: version.DefaultPluginVersioner, + Getenv: os.Getenv, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, } - err := caller.pluginMain(cmdAdd, cmdDel) + err := caller.pluginMain(cmdAdd, cmdDel, versionInfo) if err != nil { dieErr(err) } diff --git a/skel/skel_test.go b/skel/skel_test.go index e6304f5e..0431abbb 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -53,6 +53,7 @@ var _ = Describe("dispatching to the correct callback", func() { cmdAdd, cmdDel *fakeCmd dispatch *dispatcher expectedCmdArgs *CmdArgs + versionInfo version.PluginInfo ) BeforeEach(func() { @@ -67,13 +68,12 @@ var _ = Describe("dispatching to the correct callback", func() { stdin = strings.NewReader(`{ "some": "config" }`) stdout = &bytes.Buffer{} stderr = &bytes.Buffer{} - versioner := &version.BasicVersioner{CNIVersion: "9.8.7"} + versionInfo = version.PluginSupports("9.8.7") dispatch = &dispatcher{ - Getenv: func(key string) string { return environment[key] }, - Stdin: stdin, - Stdout: stdout, - Stderr: stderr, - Versioner: versioner, + Getenv: func(key string) string { return environment[key] }, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, } cmdAdd = &fakeCmd{} cmdDel = &fakeCmd{} @@ -90,7 +90,7 @@ var _ = Describe("dispatching to the correct callback", func() { var envVarChecker = func(envVar string, isRequired bool) { delete(environment, envVar) - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) if isRequired { Expect(err).To(Equal(&types.Error{ Code: 100, @@ -104,7 +104,7 @@ var _ = Describe("dispatching to the correct callback", func() { Context("when the CNI_COMMAND is ADD", func() { It("extracts env vars and stdin data and calls cmdAdd", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(1)) @@ -113,7 +113,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("does not call cmdDel", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) Expect(cmdDel.CallCount).To(Equal(0)) @@ -136,7 +136,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("reports that all of them are missing, not just the first", func() { - Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)).NotTo(Succeed()) + Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)).NotTo(Succeed()) log := stderr.String() Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n")) Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n")) @@ -152,7 +152,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("calls cmdDel with the env vars and stdin data", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) Expect(cmdDel.CallCount).To(Equal(1)) @@ -160,7 +160,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("does not call cmdAdd", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(0)) @@ -182,14 +182,17 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("prints the version to stdout", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(MatchJSON(`{ "cniVersion": "9.8.7" }`)) + Expect(stdout).To(MatchJSON(`{ + "cniVersion": "0.3.0", + "supportedVersions": ["9.8.7"] + }`)) }) It("does not call cmdAdd or cmdDel", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(0)) @@ -212,14 +215,14 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("does not call any cmd callback", func() { - dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0)) }) It("returns an error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).To(Equal(&types.Error{ Code: 100, @@ -234,14 +237,14 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("does not call any cmd callback", func() { - dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0)) }) It("wraps and returns the error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).To(Equal(&types.Error{ Code: 100, @@ -260,7 +263,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("returns the error as-is", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).To(Equal(&types.Error{ Code: 1234, @@ -275,7 +278,7 @@ var _ = Describe("dispatching to the correct callback", func() { }) It("wraps and returns the error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) Expect(err).To(Equal(&types.Error{ Code: 100, diff --git a/version/version.go b/version/version.go index 2cb075fb..72b6e9b8 100644 --- a/version/version.go +++ b/version/version.go @@ -16,27 +16,67 @@ package version import ( "encoding/json" + "fmt" "io" ) -// A PluginVersioner can encode information about its version -type PluginVersioner interface { +// Current reports the version of the CNI spec implemented by this library +func Current() string { + return "0.3.0" +} + +// PluginInfo reports information about CNI versioning +type PluginInfo interface { + // SupportedVersions returns one or more CNI spec versions that the plugin + // supports. If input is provided in one of these versions, then the plugin + // promises to use the same CNI version in its response + SupportedVersions() []string + + // Encode writes this CNI version information as JSON to the given Writer Encode(io.Writer) error } -// BasicVersioner is a PluginVersioner which reports a single cniVersion string -type BasicVersioner struct { - CNIVersion string `json:"cniVersion"` +type simple struct { + CNIVersion_ string `json:"cniVersion"` + SupportedVersions_ []string `json:"supportedVersions,omitempty"` } -func (p *BasicVersioner) Encode(w io.Writer) error { +func (p *simple) Encode(w io.Writer) error { return json.NewEncoder(w).Encode(p) } -// Current reports the version of the CNI spec implemented by this library -func Current() string { - return "0.2.0" +func (p *simple) SupportedVersions() []string { + return p.SupportedVersions_ } -// DefaultPluginVersioner reports the Current library spec version as the cniVersion -var DefaultPluginVersioner = &BasicVersioner{CNIVersion: Current()} +// PluginSupports returns a new PluginInfo that will report the given versions +// as supported +func PluginSupports(supportedVersions ...string) PluginInfo { + if len(supportedVersions) < 1 { + panic("programmer error: you must support at least one version") + } + return &simple{ + CNIVersion_: Current(), + SupportedVersions_: supportedVersions, + } +} + +func Decode(jsonBytes []byte) (PluginInfo, error) { + var info simple + err := json.Unmarshal(jsonBytes, &info) + if err != nil { + return nil, fmt.Errorf("decoding version info: %s", err) + } + if info.CNIVersion_ == "" { + return nil, fmt.Errorf("decoding version info: missing field cniVersion") + } + if len(info.SupportedVersions_) == 0 { + if info.CNIVersion_ == "0.2.0" { + return PluginSupports("0.1.0", "0.2.0"), nil + } + return nil, fmt.Errorf("decoding version info: missing field supportedVersions") + } + return &info, nil +} + +var Legacy = PluginSupports("0.1.0", "0.2.0", "0.3.0") diff --git a/version/version_suite_test.go b/version/version_suite_test.go new file mode 100644 index 00000000..25d503c8 --- /dev/null +++ b/version/version_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestVersion(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Version Suite") +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 00000000..08b89aed --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,72 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version_test + +import ( + "github.com/containernetworking/cni/pkg/version" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Decode", func() { + It("returns a PluginInfo that represents the given json bytes", func() { + pluginInfo, err := version.Decode([]byte(`{ + "cniVersion": "some-library-version", + "supportedVersions": [ "some-version", "some-other-version" ] + }`)) + Expect(err).NotTo(HaveOccurred()) + Expect(pluginInfo).NotTo(BeNil()) + Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ + "some-version", + "some-other-version", + })) + }) + + Context("when the bytes cannot be decoded as json", func() { + It("returns a meaningful error", func() { + _, err := version.Decode([]byte(`{{{`)) + Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string")) + }) + }) + + Context("when the json bytes are missing the required CNIVersion field", func() { + It("returns a meaningful error", func() { + _, err := version.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`)) + Expect(err).To(MatchError("decoding version info: missing field cniVersion")) + }) + }) + + Context("when there are no supported versions", func() { + Context("when the cniVersion is 0.2.0", func() { + It("infers the supported versions are 0.1.0 and 0.2.0", func() { + pluginInfo, err := version.Decode([]byte(`{ "cniVersion": "0.2.0" }`)) + Expect(err).NotTo(HaveOccurred()) + Expect(pluginInfo).NotTo(BeNil()) + Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ + "0.1.0", + "0.2.0", + })) + }) + }) + + Context("when the cniVersion is >= 0.3.0", func() { + It("returns a meaningful error", func() { + _, err := version.Decode([]byte(`{ "cniVersion": "0.3.0" }`)) + Expect(err).To(MatchError("decoding version info: missing field supportedVersions")) + }) + }) + }) + +}) From 383c84031e942fdbfbeb0e68f9f8d452e087c401 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Mon, 29 Aug 2016 19:20:18 -0400 Subject: [PATCH 093/134] versioning: document meaning of 'Legacy' version support --- version/version.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/version/version.go b/version/version.go index 72b6e9b8..62bf8bbb 100644 --- a/version/version.go +++ b/version/version.go @@ -79,4 +79,11 @@ func Decode(jsonBytes []byte) (PluginInfo, error) { return &info, nil } +// Legacy PluginInfo describes a plugin that is backwards compatible with the +// CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0 +// library ought to work correctly with a plugin that reports support for +// Legacy versions. +// +// Any future CNI spec versions which meet this definition will be added to +// this list. var Legacy = PluginSupports("0.1.0", "0.2.0", "0.3.0") From bd3ade0c544a4c459c343d40942603ac32e02e2c Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 2 Sep 2016 13:12:14 -0400 Subject: [PATCH 094/134] pkg/invoke: refactor plugin exec and backfill unit tests --- invoke/exec.go | 85 +++++++---------- invoke/exec_test.go | 162 +++++++++++++++++--------------- invoke/fakes/cni_args.go | 27 ++++++ invoke/fakes/raw_exec.go | 36 +++++++ invoke/fakes/version_decoder.go | 34 +++++++ invoke/raw_exec.go | 63 +++++++++++++ invoke/raw_exec_test.go | 123 ++++++++++++++++++++++++ version/version.go | 4 +- version/version_test.go | 16 +++- 9 files changed, 413 insertions(+), 137 deletions(-) create mode 100644 invoke/fakes/cni_args.go create mode 100644 invoke/fakes/raw_exec.go create mode 100644 invoke/fakes/version_decoder.go create mode 100644 invoke/raw_exec.go create mode 100644 invoke/raw_exec_test.go diff --git a/invoke/exec.go b/invoke/exec.go index 3d32ab7c..f95dec11 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -15,35 +15,41 @@ package invoke import ( - "bytes" "encoding/json" - "fmt" - "io" "os" - "os/exec" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/version" ) -func pluginErr(err error, output []byte) error { - if _, ok := err.(*exec.ExitError); ok { - emsg := types.Error{} - if perr := json.Unmarshal(output, &emsg); perr != nil { - return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) - } - details := "" - if emsg.Details != "" { - details = fmt.Sprintf("; %v", emsg.Details) - } - return fmt.Errorf("%v%v", emsg.Msg, details) - } - - return err +func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { + return defaultPluginExec.WithResult(pluginPath, netconf, args) } -func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { - stdoutBytes, err := execPlugin(pluginPath, netconf, args) +func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { + return defaultPluginExec.WithoutResult(pluginPath, netconf, args) +} + +func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) { + return defaultPluginExec.GetVersion(pluginPath) +} + +var defaultPluginExec = &PluginExec{ + RawExec: &RawExec{Stderr: os.Stderr}, + VersionDecoder: &version.Decoder{}, +} + +type PluginExec struct { + RawExec interface { + ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) + } + VersionDecoder interface { + Decode(jsonBytes []byte) (version.PluginInfo, error) + } +} + +func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { + stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) if err != nil { return nil, err } @@ -53,44 +59,17 @@ func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*typ return res, err } -func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { - _, err := execPlugin(pluginPath, netconf, args) +func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { + _, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) return err } -func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) { - stdoutBytes, err := execPlugin(pluginPath, nil, &Args{Command: "VERSION"}) +func (e *PluginExec) GetVersion(pluginPath string) (version.PluginInfo, error) { + args := &Args{Command: "VERSION"} + stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv()) if err != nil { return nil, err } - return version.Decode(stdoutBytes) -} - -func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) { - return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) -} - -var defaultRawExec = &RawExec{Stderr: os.Stderr} - -type RawExec struct { - Stderr io.Writer -} - -func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Env: environ, - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(stdinData), - Stdout: stdout, - Stderr: e.Stderr, - } - if err := c.Run(); err != nil { - return nil, pluginErr(err, stdout.Bytes()) - } - - return stdout.Bytes(), nil + return e.VersionDecoder.Decode(stdoutBytes) } diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 7df60a11..bff3fb73 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -15,109 +15,115 @@ package invoke_test import ( - "bytes" - "io/ioutil" - "os" + "errors" "github.com/containernetworking/cni/pkg/invoke" - - noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" + "github.com/containernetworking/cni/pkg/invoke/fakes" + "github.com/containernetworking/cni/pkg/version" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -var _ = Describe("RawExec", func() { +var _ = Describe("Executing a plugin", func() { var ( - debugFileName string - debug *noop_debug.Debug - environ []string - stdin []byte - execer *invoke.RawExec + pluginExec *invoke.PluginExec + rawExec *fakes.RawExec + versionDecoder *fakes.VersionDecoder + + pluginPath string + netconf []byte + cniargs *fakes.CNIArgs ) - const reportResult = `{ "some": "result" }` - BeforeEach(func() { - debugFile, err := ioutil.TempFile("", "cni_debug") - Expect(err).NotTo(HaveOccurred()) - Expect(debugFile.Close()).To(Succeed()) - debugFileName = debugFile.Name() + rawExec = &fakes.RawExec{} + rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ip4": { "ip": "1.2.3.4/24" } }`) - debug = &noop_debug.Debug{ - ReportResult: reportResult, - ReportStderr: "some stderr message", + versionDecoder = &fakes.VersionDecoder{} + versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") + + pluginExec = &invoke.PluginExec{ + RawExec: rawExec, + VersionDecoder: versionDecoder, } - Expect(debug.WriteDebug(debugFileName)).To(Succeed()) - - environ = []string{ - "CNI_COMMAND=ADD", - "CNI_CONTAINERID=some-container-id", - "CNI_ARGS=DEBUG=" + debugFileName, - "CNI_NETNS=/some/netns/path", - "CNI_PATH=/some/bin/path", - "CNI_IFNAME=some-eth0", - } - stdin = []byte(`{"some":"stdin-json"}`) - execer = &invoke.RawExec{} + pluginPath = "/some/plugin/path" + netconf = []byte(`{ "some": "stdin" }`) + cniargs = &fakes.CNIArgs{} + cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) - AfterEach(func() { - Expect(os.Remove(debugFileName)).To(Succeed()) - }) - - It("runs the plugin with the given stdin and environment", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).NotTo(HaveOccurred()) - - debug, err := noop_debug.ReadDebug(debugFileName) - Expect(err).NotTo(HaveOccurred()) - Expect(debug.Command).To(Equal("ADD")) - Expect(debug.CmdArgs.StdinData).To(Equal(stdin)) - Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path")) - }) - - It("returns the resulting stdout as bytes", func() { - resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).NotTo(HaveOccurred()) - - Expect(resultBytes).To(BeEquivalentTo(reportResult)) - }) - - Context("when the Stderr writer is set", func() { - var stderrBuffer *bytes.Buffer - - BeforeEach(func() { - stderrBuffer = &bytes.Buffer{} - execer.Stderr = stderrBuffer - }) - - It("forwards any stderr bytes to the Stderr writer", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Describe("returning a result", func() { + It("unmarshals the result bytes into the Result type", func() { + result, err := pluginExec.WithResult(pluginPath, netconf, cniargs) Expect(err).NotTo(HaveOccurred()) + Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4")) + }) - Expect(stderrBuffer.String()).To(Equal("some stderr message")) + It("passes its arguments through to the rawExec", func() { + pluginExec.WithResult(pluginPath, netconf, cniargs) + Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) + Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) + Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) + }) + + Context("when the rawExec fails", func() { + BeforeEach(func() { + rawExec.ExecPluginCall.Returns.Error = errors.New("banana") + }) + It("returns the error", func() { + _, err := pluginExec.WithResult(pluginPath, netconf, cniargs) + Expect(err).To(MatchError("banana")) + }) }) }) - Context("when the plugin errors", func() { + Describe("without returning a result", func() { + It("passes its arguments through to the rawExec", func() { + pluginExec.WithoutResult(pluginPath, netconf, cniargs) + Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) + Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) + Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) + }) + + Context("when the rawExec fails", func() { + BeforeEach(func() { + rawExec.ExecPluginCall.Returns.Error = errors.New("banana") + }) + It("returns the error", func() { + err := pluginExec.WithoutResult(pluginPath, netconf, cniargs) + Expect(err).To(MatchError("banana")) + }) + }) + }) + + Describe("discovering the plugin version", func() { BeforeEach(func() { - debug.ReportError = "banana" - Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`) }) - It("wraps and returns the error", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("banana")) + It("execs the plugin with the command VERSION", func() { + pluginExec.GetVersion(pluginPath) + Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) + Expect(rawExec.ExecPluginCall.Received.StdinData).To(BeNil()) + Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION")) }) - }) - Context("when the system is unable to execute the plugin", func() { - It("returns the error", func() { - _, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path"))) + It("decodes and returns the version info", func() { + versionInfo, err := pluginExec.GetVersion(pluginPath) + Expect(err).NotTo(HaveOccurred()) + Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"})) + Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`)) + }) + + Context("when the rawExec fails", func() { + BeforeEach(func() { + rawExec.ExecPluginCall.Returns.Error = errors.New("banana") + }) + It("returns the error", func() { + _, err := pluginExec.GetVersion(pluginPath) + Expect(err).To(MatchError("banana")) + }) }) }) }) diff --git a/invoke/fakes/cni_args.go b/invoke/fakes/cni_args.go new file mode 100644 index 00000000..5b1ba29e --- /dev/null +++ b/invoke/fakes/cni_args.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fakes + +type CNIArgs struct { + AsEnvCall struct { + Returns struct { + Env []string + } + } +} + +func (a *CNIArgs) AsEnv() []string { + return a.AsEnvCall.Returns.Env +} diff --git a/invoke/fakes/raw_exec.go b/invoke/fakes/raw_exec.go new file mode 100644 index 00000000..5432cdf7 --- /dev/null +++ b/invoke/fakes/raw_exec.go @@ -0,0 +1,36 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fakes + +type RawExec struct { + ExecPluginCall struct { + Received struct { + PluginPath string + StdinData []byte + Environ []string + } + Returns struct { + ResultBytes []byte + Error error + } + } +} + +func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { + e.ExecPluginCall.Received.PluginPath = pluginPath + e.ExecPluginCall.Received.StdinData = stdinData + e.ExecPluginCall.Received.Environ = environ + return e.ExecPluginCall.Returns.ResultBytes, e.ExecPluginCall.Returns.Error +} diff --git a/invoke/fakes/version_decoder.go b/invoke/fakes/version_decoder.go new file mode 100644 index 00000000..72d29733 --- /dev/null +++ b/invoke/fakes/version_decoder.go @@ -0,0 +1,34 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fakes + +import "github.com/containernetworking/cni/pkg/version" + +type VersionDecoder struct { + DecodeCall struct { + Received struct { + JSONBytes []byte + } + Returns struct { + PluginInfo version.PluginInfo + Error error + } + } +} + +func (e *VersionDecoder) Decode(jsonData []byte) (version.PluginInfo, error) { + e.DecodeCall.Received.JSONBytes = jsonData + return e.DecodeCall.Returns.PluginInfo, e.DecodeCall.Returns.Error +} diff --git a/invoke/raw_exec.go b/invoke/raw_exec.go new file mode 100644 index 00000000..d1bd860d --- /dev/null +++ b/invoke/raw_exec.go @@ -0,0 +1,63 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os/exec" + + "github.com/containernetworking/cni/pkg/types" +) + +type RawExec struct { + Stderr io.Writer +} + +func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { + stdout := &bytes.Buffer{} + + c := exec.Cmd{ + Env: environ, + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(stdinData), + Stdout: stdout, + Stderr: e.Stderr, + } + if err := c.Run(); err != nil { + return nil, pluginErr(err, stdout.Bytes()) + } + + return stdout.Bytes(), nil +} + +func pluginErr(err error, output []byte) error { + if _, ok := err.(*exec.ExitError); ok { + emsg := types.Error{} + if perr := json.Unmarshal(output, &emsg); perr != nil { + return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) + } + details := "" + if emsg.Details != "" { + details = fmt.Sprintf("; %v", emsg.Details) + } + return fmt.Errorf("%v%v", emsg.Msg, details) + } + + return err +} diff --git a/invoke/raw_exec_test.go b/invoke/raw_exec_test.go new file mode 100644 index 00000000..7df60a11 --- /dev/null +++ b/invoke/raw_exec_test.go @@ -0,0 +1,123 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke_test + +import ( + "bytes" + "io/ioutil" + "os" + + "github.com/containernetworking/cni/pkg/invoke" + + noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("RawExec", func() { + var ( + debugFileName string + debug *noop_debug.Debug + environ []string + stdin []byte + execer *invoke.RawExec + ) + + const reportResult = `{ "some": "result" }` + + BeforeEach(func() { + debugFile, err := ioutil.TempFile("", "cni_debug") + Expect(err).NotTo(HaveOccurred()) + Expect(debugFile.Close()).To(Succeed()) + debugFileName = debugFile.Name() + + debug = &noop_debug.Debug{ + ReportResult: reportResult, + ReportStderr: "some stderr message", + } + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + + environ = []string{ + "CNI_COMMAND=ADD", + "CNI_CONTAINERID=some-container-id", + "CNI_ARGS=DEBUG=" + debugFileName, + "CNI_NETNS=/some/netns/path", + "CNI_PATH=/some/bin/path", + "CNI_IFNAME=some-eth0", + } + stdin = []byte(`{"some":"stdin-json"}`) + execer = &invoke.RawExec{} + }) + + AfterEach(func() { + Expect(os.Remove(debugFileName)).To(Succeed()) + }) + + It("runs the plugin with the given stdin and environment", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + debug, err := noop_debug.ReadDebug(debugFileName) + Expect(err).NotTo(HaveOccurred()) + Expect(debug.Command).To(Equal("ADD")) + Expect(debug.CmdArgs.StdinData).To(Equal(stdin)) + Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path")) + }) + + It("returns the resulting stdout as bytes", func() { + resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + Expect(resultBytes).To(BeEquivalentTo(reportResult)) + }) + + Context("when the Stderr writer is set", func() { + var stderrBuffer *bytes.Buffer + + BeforeEach(func() { + stderrBuffer = &bytes.Buffer{} + execer.Stderr = stderrBuffer + }) + + It("forwards any stderr bytes to the Stderr writer", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).NotTo(HaveOccurred()) + + Expect(stderrBuffer.String()).To(Equal("some stderr message")) + }) + }) + + Context("when the plugin errors", func() { + BeforeEach(func() { + debug.ReportError = "banana" + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + }) + + It("wraps and returns the error", func() { + _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("banana")) + }) + }) + + Context("when the system is unable to execute the plugin", func() { + It("returns the error", func() { + _, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path"))) + }) + }) +}) diff --git a/version/version.go b/version/version.go index 62bf8bbb..cdb531c0 100644 --- a/version/version.go +++ b/version/version.go @@ -61,7 +61,9 @@ func PluginSupports(supportedVersions ...string) PluginInfo { } } -func Decode(jsonBytes []byte) (PluginInfo, error) { +type Decoder struct{} + +func (_ *Decoder) Decode(jsonBytes []byte) (PluginInfo, error) { var info simple err := json.Unmarshal(jsonBytes, &info) if err != nil { diff --git a/version/version_test.go b/version/version_test.go index 08b89aed..98a386d7 100644 --- a/version/version_test.go +++ b/version/version_test.go @@ -21,8 +21,14 @@ import ( ) var _ = Describe("Decode", func() { + var decoder *version.Decoder + + BeforeEach(func() { + decoder = &version.Decoder{} + }) + It("returns a PluginInfo that represents the given json bytes", func() { - pluginInfo, err := version.Decode([]byte(`{ + pluginInfo, err := decoder.Decode([]byte(`{ "cniVersion": "some-library-version", "supportedVersions": [ "some-version", "some-other-version" ] }`)) @@ -36,14 +42,14 @@ var _ = Describe("Decode", func() { Context("when the bytes cannot be decoded as json", func() { It("returns a meaningful error", func() { - _, err := version.Decode([]byte(`{{{`)) + _, err := decoder.Decode([]byte(`{{{`)) Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string")) }) }) Context("when the json bytes are missing the required CNIVersion field", func() { It("returns a meaningful error", func() { - _, err := version.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`)) + _, err := decoder.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`)) Expect(err).To(MatchError("decoding version info: missing field cniVersion")) }) }) @@ -51,7 +57,7 @@ var _ = Describe("Decode", func() { Context("when there are no supported versions", func() { Context("when the cniVersion is 0.2.0", func() { It("infers the supported versions are 0.1.0 and 0.2.0", func() { - pluginInfo, err := version.Decode([]byte(`{ "cniVersion": "0.2.0" }`)) + pluginInfo, err := decoder.Decode([]byte(`{ "cniVersion": "0.2.0" }`)) Expect(err).NotTo(HaveOccurred()) Expect(pluginInfo).NotTo(BeNil()) Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ @@ -63,7 +69,7 @@ var _ = Describe("Decode", func() { Context("when the cniVersion is >= 0.3.0", func() { It("returns a meaningful error", func() { - _, err := version.Decode([]byte(`{ "cniVersion": "0.3.0" }`)) + _, err := decoder.Decode([]byte(`{ "cniVersion": "0.3.0" }`)) Expect(err).To(MatchError("decoding version info: missing field supportedVersions")) }) }) From 2142c076abc32ba3d929ba7148328d4f363b395a Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 23 Aug 2016 22:57:00 -0700 Subject: [PATCH 095/134] versioning: adds tooling to compile a program against a given old CNI version Allows us to write tests that cover interactions between components of differing versions --- version/testhelpers/testhelpers.go | 156 ++++++++++++++++++ version/testhelpers/testhelpers_suite_test.go | 27 +++ version/testhelpers/testhelpers_test.go | 106 ++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 version/testhelpers/testhelpers.go create mode 100644 version/testhelpers/testhelpers_suite_test.go create mode 100644 version/testhelpers/testhelpers_test.go diff --git a/version/testhelpers/testhelpers.go b/version/testhelpers/testhelpers.go new file mode 100644 index 00000000..773d0120 --- /dev/null +++ b/version/testhelpers/testhelpers.go @@ -0,0 +1,156 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package testhelpers supports testing of CNI components of different versions +// +// For example, to build a plugin against an old version of the CNI library, +// we can pass the plugin's source and the old git commit reference to BuildAt. +// We could then test how the built binary responds when called by the latest +// version of this library. +package testhelpers + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +const packageBaseName = "github.com/containernetworking/cni" + +func run(cmd *exec.Cmd) error { + out, err := cmd.CombinedOutput() + if err != nil { + command := strings.Join(cmd.Args, " ") + return fmt.Errorf("running %q: %s", command, out) + } + return nil +} + +func goBuildEnviron(gopath string) []string { + environ := os.Environ() + for i, kvp := range environ { + if strings.HasPrefix(kvp, "GOPATH=") { + environ[i] = "GOPATH=" + gopath + return environ + } + } + environ = append(environ, "GOPATH="+gopath) + return environ +} + +func buildGoProgram(gopath, packageName, outputFilePath string) error { + cmd := exec.Command("go", "build", "-o", outputFilePath, packageName) + cmd.Env = goBuildEnviron(gopath) + return run(cmd) +} + +func createSingleFilePackage(gopath, packageName string, fileContents []byte) error { + dirName := filepath.Join(gopath, "src", packageName) + err := os.MkdirAll(dirName, 0700) + if err != nil { + return err + } + + return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600) +} + +func removePackage(gopath, packageName string) error { + dirName := filepath.Join(gopath, "src", packageName) + return os.RemoveAll(dirName) +} + +func isRepoRoot(path string) bool { + _, err := ioutil.ReadDir(filepath.Join(path, ".git")) + return (err == nil) && (filepath.Base(path) == "cni") +} + +func LocateCurrentGitRepo() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + + for i := 0; i < 5; i++ { + if isRepoRoot(dir) { + return dir, nil + } + + dir, err = filepath.Abs(filepath.Dir(dir)) + if err != nil { + return "", fmt.Errorf("abs(dir(%q)): %s", dir, err) + } + } + + return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir) +} + +func gitCloneThisRepo(cloneDestination string) error { + err := os.MkdirAll(cloneDestination, 0700) + if err != nil { + return err + } + + currentGitRepo, err := LocateCurrentGitRepo() + if err != nil { + return err + } + + return run(exec.Command("git", "clone", currentGitRepo, cloneDestination)) +} + +func gitCheckout(localRepo string, gitRef string) error { + return run(exec.Command("git", "-C", localRepo, "checkout", gitRef)) +} + +// BuildAt builds the go programSource using the version of the CNI library +// at gitRef, and saves the resulting binary file at outputFilePath +func BuildAt(programSource []byte, gitRef string, outputFilePath string) error { + tempGoPath, err := ioutil.TempDir("", "cni-git-") + if err != nil { + return err + } + defer os.RemoveAll(tempGoPath) + + cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName) + err = gitCloneThisRepo(cloneDestination) + if err != nil { + return err + } + + err = gitCheckout(cloneDestination, gitRef) + if err != nil { + return err + } + + rand.Seed(time.Now().UnixNano()) + testPackageName := fmt.Sprintf("test-package-%x", rand.Int31()) + + err = createSingleFilePackage(tempGoPath, testPackageName, programSource) + if err != nil { + return err + } + defer removePackage(tempGoPath, testPackageName) + + err = buildGoProgram(tempGoPath, testPackageName, outputFilePath) + if err != nil { + return err + } + + return nil +} diff --git a/version/testhelpers/testhelpers_suite_test.go b/version/testhelpers/testhelpers_suite_test.go new file mode 100644 index 00000000..72f65f9c --- /dev/null +++ b/version/testhelpers/testhelpers_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testhelpers_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTesthelpers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testhelpers Suite") +} diff --git a/version/testhelpers/testhelpers_test.go b/version/testhelpers/testhelpers_test.go new file mode 100644 index 00000000..3473cd59 --- /dev/null +++ b/version/testhelpers/testhelpers_test.go @@ -0,0 +1,106 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package testhelpers_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/containernetworking/cni/pkg/version/testhelpers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("BuildAt", func() { + var ( + gitRef string + outputFilePath string + outputDir string + programSource []byte + ) + BeforeEach(func() { + programSource = []byte(`package main + +import "github.com/containernetworking/cni/pkg/skel" + +func c(_ *skel.CmdArgs) error { return nil } + +func main() { skel.PluginMain(c, c) } +`) + gitRef = "f4364185253" + + var err error + outputDir, err = ioutil.TempDir("", "bin") + Expect(err).NotTo(HaveOccurred()) + outputFilePath = filepath.Join(outputDir, "some-binary") + }) + + AfterEach(func() { + Expect(os.RemoveAll(outputDir)).To(Succeed()) + }) + + It("builds the provided source code using the CNI library at the given git ref", func() { + Expect(outputFilePath).NotTo(BeAnExistingFile()) + + err := testhelpers.BuildAt(programSource, gitRef, outputFilePath) + Expect(err).NotTo(HaveOccurred()) + + Expect(outputFilePath).To(BeAnExistingFile()) + + cmd := exec.Command(outputFilePath) + cmd.Env = []string{"CNI_COMMAND=VERSION"} + output, err := cmd.CombinedOutput() + Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{})) + Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION")) + }) +}) + +var _ = Describe("LocateCurrentGitRepo", func() { + It("returns the path to the root of the CNI git repo", func() { + path, err := testhelpers.LocateCurrentGitRepo() + Expect(err).NotTo(HaveOccurred()) + + AssertItIsTheCNIRepoRoot(path) + }) + + Context("when run from a different directory", func() { + BeforeEach(func() { + os.Chdir("..") + }) + + It("still finds the CNI repo root", func() { + path, err := testhelpers.LocateCurrentGitRepo() + Expect(err).NotTo(HaveOccurred()) + + AssertItIsTheCNIRepoRoot(path) + }) + }) +}) + +func AssertItIsTheCNIRepoRoot(path string) { + Expect(path).To(BeADirectory()) + files, err := ioutil.ReadDir(path) + Expect(err).NotTo(HaveOccurred()) + + names := []string{} + for _, file := range files { + names = append(names, file.Name()) + } + + Expect(names).To(ContainElement("SPEC.md")) + Expect(names).To(ContainElement("libcni")) + Expect(names).To(ContainElement("cnitool")) +} From f467134838ab8542ea682cad6ee54e3f35f9636b Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 2 Sep 2016 16:01:22 -0400 Subject: [PATCH 096/134] invoke: correctly infer version for 0.1.0-vintage plugins Older plugins return a known error when issued the VERSION command. Capture this error and report it as a 0.1.0 version plugin. --- invoke/exec.go | 12 +++++- invoke/exec_integration_test.go | 72 +++++++++++++++++++++++++++++++++ invoke/exec_test.go | 2 +- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 invoke/exec_integration_test.go diff --git a/invoke/exec.go b/invoke/exec.go index f95dec11..fe792b62 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -65,9 +65,19 @@ func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIAr } func (e *PluginExec) GetVersion(pluginPath string) (version.PluginInfo, error) { - args := &Args{Command: "VERSION"} + args := &Args{ + Command: "VERSION", + + // set fake values required by plugins built against an older version of skel + NetNS: "/tmp/not/a/container", + IfName: "not-an-interface", + Path: "/tmp/not/a/path", + } stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv()) if err != nil { + if err.Error() == "unknown CNI_COMMAND: VERSION" { + return version.PluginSupports("0.1.0"), nil + } return nil, err } diff --git a/invoke/exec_integration_test.go b/invoke/exec_integration_test.go new file mode 100644 index 00000000..c16090ad --- /dev/null +++ b/invoke/exec_integration_test.go @@ -0,0 +1,72 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/invoke" + "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/cni/pkg/version/testhelpers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetVersion, integration tests", func() { + var ( + pluginDir string + pluginPath string + ) + + BeforeEach(func() { + pluginDir, err := ioutil.TempDir("", "plugins") + Expect(err).NotTo(HaveOccurred()) + pluginPath = filepath.Join(pluginDir, "test-plugin") + }) + + AfterEach(func() { + Expect(os.RemoveAll(pluginDir)).To(Succeed()) + }) + + DescribeTable("correctly reporting plugin versions", + func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) { + Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed()) + versionInfo, err := invoke.ExecPluginForVersion(pluginPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions())) + }, + Entry("old plugin, before VERSION was introduced", git_ref_v010, plugin_source_v010, version.PluginSupports("0.1.0")), + Entry("when VERSION was introduced", git_ref_v020, plugin_source_v010, version.PluginSupports("0.1.0", "0.2.0")), + ) +}) + +// a minimal 0.1.0 / 0.2.0 plugin +const plugin_source_v010 = `package main + +import "github.com/containernetworking/cni/pkg/skel" +import "fmt" + +func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil } + +func main() { skel.PluginMain(c, c) } +` + +const git_ref_v010 = "2c482f4" +const git_ref_v020 = "349d66d" diff --git a/invoke/exec_test.go b/invoke/exec_test.go index bff3fb73..7b591f17 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -25,7 +25,7 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Executing a plugin", func() { +var _ = Describe("Executing a plugin, unit tests", func() { var ( pluginExec *invoke.PluginExec rawExec *fakes.RawExec From 2f9740739668061166b630b7da3337342b765ac6 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 2 Sep 2016 16:39:01 -0400 Subject: [PATCH 097/134] invoke: better name and unit test coverage for GetVersionInfo --- invoke/exec.go | 12 ++++++------ invoke/exec_integration_test.go | 2 +- invoke/exec_test.go | 28 +++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/invoke/exec.go b/invoke/exec.go index fe792b62..68efbb9b 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -30,8 +30,8 @@ func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) er return defaultPluginExec.WithoutResult(pluginPath, netconf, args) } -func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) { - return defaultPluginExec.GetVersion(pluginPath) +func GetVersionInfo(pluginPath string) (version.PluginInfo, error) { + return defaultPluginExec.GetVersionInfo(pluginPath) } var defaultPluginExec = &PluginExec{ @@ -64,14 +64,14 @@ func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIAr return err } -func (e *PluginExec) GetVersion(pluginPath string) (version.PluginInfo, error) { +func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) { args := &Args{ Command: "VERSION", // set fake values required by plugins built against an older version of skel - NetNS: "/tmp/not/a/container", - IfName: "not-an-interface", - Path: "/tmp/not/a/path", + NetNS: "dummy", + IfName: "dummy", + Path: "dummy", } stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv()) if err != nil { diff --git a/invoke/exec_integration_test.go b/invoke/exec_integration_test.go index c16090ad..f02374c0 100644 --- a/invoke/exec_integration_test.go +++ b/invoke/exec_integration_test.go @@ -47,7 +47,7 @@ var _ = Describe("GetVersion, integration tests", func() { DescribeTable("correctly reporting plugin versions", func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) { Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed()) - versionInfo, err := invoke.ExecPluginForVersion(pluginPath) + versionInfo, err := invoke.GetVersionInfo(pluginPath) Expect(err).NotTo(HaveOccurred()) Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions())) diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 7b591f17..94007d7c 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -103,14 +103,14 @@ var _ = Describe("Executing a plugin, unit tests", func() { }) It("execs the plugin with the command VERSION", func() { - pluginExec.GetVersion(pluginPath) + pluginExec.GetVersionInfo(pluginPath) Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) Expect(rawExec.ExecPluginCall.Received.StdinData).To(BeNil()) Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION")) }) It("decodes and returns the version info", func() { - versionInfo, err := pluginExec.GetVersion(pluginPath) + versionInfo, err := pluginExec.GetVersionInfo(pluginPath) Expect(err).NotTo(HaveOccurred()) Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"})) Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`)) @@ -121,9 +121,31 @@ var _ = Describe("Executing a plugin, unit tests", func() { rawExec.ExecPluginCall.Returns.Error = errors.New("banana") }) It("returns the error", func() { - _, err := pluginExec.GetVersion(pluginPath) + _, err := pluginExec.GetVersionInfo(pluginPath) Expect(err).To(MatchError("banana")) }) }) + + Context("when the plugin is too old to recognize the VERSION command", func() { + BeforeEach(func() { + rawExec.ExecPluginCall.Returns.Error = errors.New("unknown CNI_COMMAND: VERSION") + }) + + It("interprets the error as a 0.1.0 version", func() { + versionInfo, err := pluginExec.GetVersionInfo(pluginPath) + Expect(err).NotTo(HaveOccurred()) + Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0")) + }) + + It("sets dummy values for env vars required by very old plugins", func() { + pluginExec.GetVersionInfo(pluginPath) + + env := rawExec.ExecPluginCall.Received.Environ + Expect(env).To(ContainElement("CNI_NETNS=dummy")) + Expect(env).To(ContainElement("CNI_IFNAME=dummy")) + Expect(env).To(ContainElement("CNI_PATH=dummy")) + }) + }) + }) }) From bf6948da19ebd331535e6015c6c25139ff65c2e9 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 6 Sep 2016 11:22:27 -0400 Subject: [PATCH 098/134] versioning: misc cleanups highlights: - NetConf struct finally includes cniVersion field - improve test coverage of current version report behavior - godoc a few key functions - allow tests to control version list reported by no-op plugin --- invoke/exec.go | 6 +- ...est.go => get_version_integration_test.go} | 17 ++++ types/types.go | 2 + version/plugin.go | 77 +++++++++++++++++++ version/{version_test.go => plugin_test.go} | 6 +- version/version.go | 62 --------------- 6 files changed, 104 insertions(+), 66 deletions(-) rename invoke/{exec_integration_test.go => get_version_integration_test.go} (78%) create mode 100644 version/plugin.go rename version/{version_test.go => plugin_test.go} (94%) diff --git a/invoke/exec.go b/invoke/exec.go index 68efbb9b..7eb06156 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -36,7 +36,7 @@ func GetVersionInfo(pluginPath string) (version.PluginInfo, error) { var defaultPluginExec = &PluginExec{ RawExec: &RawExec{Stderr: os.Stderr}, - VersionDecoder: &version.Decoder{}, + VersionDecoder: &version.PluginDecoder{}, } type PluginExec struct { @@ -64,6 +64,10 @@ func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIAr return err } +// GetVersionInfo returns the version information available about the plugin. +// For recent-enough plugins, it uses the information returned by the VERSION +// command. For older plugins which do not recognize that command, it reports +// version 0.1.0 func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) { args := &Args{ Command: "VERSION", diff --git a/invoke/exec_integration_test.go b/invoke/get_version_integration_test.go similarity index 78% rename from invoke/exec_integration_test.go rename to invoke/get_version_integration_test.go index f02374c0..d10826dd 100644 --- a/invoke/exec_integration_test.go +++ b/invoke/get_version_integration_test.go @@ -54,9 +54,26 @@ var _ = Describe("GetVersion, integration tests", func() { }, Entry("old plugin, before VERSION was introduced", git_ref_v010, plugin_source_v010, version.PluginSupports("0.1.0")), Entry("when VERSION was introduced", git_ref_v020, plugin_source_v010, version.PluginSupports("0.1.0", "0.2.0")), + Entry("when plugins report their own version support", git_ref_v030, plugin_source_v030, version.PluginSupports("0.3.0", "0.999.0")), + Entry("HEAD", "HEAD", plugin_source_v030, version.PluginSupports("0.3.0", "0.999.0")), ) }) +// a 0.3.0 plugin that can report its own versions +const plugin_source_v030 = `package main + +import ( + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/version" + "fmt" +) + +func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil } + +func main() { skel.PluginMain(c, c, version.PluginSupports("0.3.0", "0.999.0")) } +` +const git_ref_v030 = "bf31ed15" + // a minimal 0.1.0 / 0.2.0 plugin const plugin_source_v010 = `package main diff --git a/types/types.go b/types/types.go index 6948dcb1..ba02580a 100644 --- a/types/types.go +++ b/types/types.go @@ -57,6 +57,8 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { // NetConf describes a network. type NetConf struct { + CNIVersion string `json:"cniVersion,omitempty"` + Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` IPAM struct { diff --git a/version/plugin.go b/version/plugin.go new file mode 100644 index 00000000..9bd7dc83 --- /dev/null +++ b/version/plugin.go @@ -0,0 +1,77 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "encoding/json" + "fmt" + "io" +) + +// PluginInfo reports information about CNI versioning +type PluginInfo interface { + // SupportedVersions returns one or more CNI spec versions that the plugin + // supports. If input is provided in one of these versions, then the plugin + // promises to use the same CNI version in its response + SupportedVersions() []string + + // Encode writes this CNI version information as JSON to the given Writer + Encode(io.Writer) error +} + +type pluginInfo struct { + CNIVersion_ string `json:"cniVersion"` + SupportedVersions_ []string `json:"supportedVersions,omitempty"` +} + +func (p *pluginInfo) Encode(w io.Writer) error { + return json.NewEncoder(w).Encode(p) +} + +func (p *pluginInfo) SupportedVersions() []string { + return p.SupportedVersions_ +} + +// PluginSupports returns a new PluginInfo that will report the given versions +// as supported +func PluginSupports(supportedVersions ...string) PluginInfo { + if len(supportedVersions) < 1 { + panic("programmer error: you must support at least one version") + } + return &pluginInfo{ + CNIVersion_: Current(), + SupportedVersions_: supportedVersions, + } +} + +type PluginDecoder struct{} + +func (_ *PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { + var info pluginInfo + err := json.Unmarshal(jsonBytes, &info) + if err != nil { + return nil, fmt.Errorf("decoding version info: %s", err) + } + if info.CNIVersion_ == "" { + return nil, fmt.Errorf("decoding version info: missing field cniVersion") + } + if len(info.SupportedVersions_) == 0 { + if info.CNIVersion_ == "0.2.0" { + return PluginSupports("0.1.0", "0.2.0"), nil + } + return nil, fmt.Errorf("decoding version info: missing field supportedVersions") + } + return &info, nil +} diff --git a/version/version_test.go b/version/plugin_test.go similarity index 94% rename from version/version_test.go rename to version/plugin_test.go index 98a386d7..a58bd35a 100644 --- a/version/version_test.go +++ b/version/plugin_test.go @@ -20,11 +20,11 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Decode", func() { - var decoder *version.Decoder +var _ = Describe("Decoding versions reported by a plugin", func() { + var decoder *version.PluginDecoder BeforeEach(func() { - decoder = &version.Decoder{} + decoder = &version.PluginDecoder{} }) It("returns a PluginInfo that represents the given json bytes", func() { diff --git a/version/version.go b/version/version.go index cdb531c0..5f937f7e 100644 --- a/version/version.go +++ b/version/version.go @@ -14,73 +14,11 @@ package version -import ( - "encoding/json" - "fmt" - "io" -) - // Current reports the version of the CNI spec implemented by this library func Current() string { return "0.3.0" } -// PluginInfo reports information about CNI versioning -type PluginInfo interface { - // SupportedVersions returns one or more CNI spec versions that the plugin - // supports. If input is provided in one of these versions, then the plugin - // promises to use the same CNI version in its response - SupportedVersions() []string - - // Encode writes this CNI version information as JSON to the given Writer - Encode(io.Writer) error -} - -type simple struct { - CNIVersion_ string `json:"cniVersion"` - SupportedVersions_ []string `json:"supportedVersions,omitempty"` -} - -func (p *simple) Encode(w io.Writer) error { - return json.NewEncoder(w).Encode(p) -} - -func (p *simple) SupportedVersions() []string { - return p.SupportedVersions_ -} - -// PluginSupports returns a new PluginInfo that will report the given versions -// as supported -func PluginSupports(supportedVersions ...string) PluginInfo { - if len(supportedVersions) < 1 { - panic("programmer error: you must support at least one version") - } - return &simple{ - CNIVersion_: Current(), - SupportedVersions_: supportedVersions, - } -} - -type Decoder struct{} - -func (_ *Decoder) Decode(jsonBytes []byte) (PluginInfo, error) { - var info simple - err := json.Unmarshal(jsonBytes, &info) - if err != nil { - return nil, fmt.Errorf("decoding version info: %s", err) - } - if info.CNIVersion_ == "" { - return nil, fmt.Errorf("decoding version info: missing field cniVersion") - } - if len(info.SupportedVersions_) == 0 { - if info.CNIVersion_ == "0.2.0" { - return PluginSupports("0.1.0", "0.2.0"), nil - } - return nil, fmt.Errorf("decoding version info: missing field supportedVersions") - } - return &info, nil -} - // Legacy PluginInfo describes a plugin that is backwards compatible with the // CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0 // library ought to work correctly with a plugin that reports support for From de91f31ae7a7edeac91f48ad8c7c408a5f594a36 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 6 Sep 2016 15:24:12 -0400 Subject: [PATCH 099/134] versioning: revert spec version to 0.2.0 --- invoke/get_version_integration_test.go | 40 ++++++++++++++------ skel/skel_test.go | 2 +- version/plugin_test.go | 51 +++++++++++++++----------- version/version.go | 6 +-- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/invoke/get_version_integration_test.go b/invoke/get_version_integration_test.go index d10826dd..7e58a9be 100644 --- a/invoke/get_version_integration_test.go +++ b/invoke/get_version_integration_test.go @@ -52,15 +52,33 @@ var _ = Describe("GetVersion, integration tests", func() { Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions())) }, - Entry("old plugin, before VERSION was introduced", git_ref_v010, plugin_source_v010, version.PluginSupports("0.1.0")), - Entry("when VERSION was introduced", git_ref_v020, plugin_source_v010, version.PluginSupports("0.1.0", "0.2.0")), - Entry("when plugins report their own version support", git_ref_v030, plugin_source_v030, version.PluginSupports("0.3.0", "0.999.0")), - Entry("HEAD", "HEAD", plugin_source_v030, version.PluginSupports("0.3.0", "0.999.0")), + + Entry("historical: before VERSION was introduced", + git_ref_v010, plugin_source_no_custom_versions, + version.PluginSupports("0.1.0"), + ), + + Entry("historical: when VERSION was introduced but plugins couldn't customize it", + git_ref_v020_no_custom_versions, plugin_source_no_custom_versions, + version.PluginSupports("0.1.0", "0.2.0"), + ), + + Entry("historical: when plugins started reporting their own version list", + git_ref_v020_custom_versions, plugin_source_v020_custom_versions, + version.PluginSupports("0.2.0", "0.999.0"), + ), + + // this entry tracks the current behavior. Before you change it, ensure + // that its previous behavior is captured in the most recent "historical" entry + Entry("current", + "HEAD", plugin_source_v020_custom_versions, + version.PluginSupports("0.2.0", "0.999.0"), + ), ) }) -// a 0.3.0 plugin that can report its own versions -const plugin_source_v030 = `package main +// a 0.2.0 plugin that can report its own versions +const plugin_source_v020_custom_versions = `package main import ( "github.com/containernetworking/cni/pkg/skel" @@ -70,12 +88,12 @@ import ( func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil } -func main() { skel.PluginMain(c, c, version.PluginSupports("0.3.0", "0.999.0")) } +func main() { skel.PluginMain(c, c, version.PluginSupports("0.2.0", "0.999.0")) } ` -const git_ref_v030 = "bf31ed15" +const git_ref_v020_custom_versions = "bf31ed15" -// a minimal 0.1.0 / 0.2.0 plugin -const plugin_source_v010 = `package main +// a minimal 0.1.0 / 0.2.0 plugin that cannot report it's own version support +const plugin_source_no_custom_versions = `package main import "github.com/containernetworking/cni/pkg/skel" import "fmt" @@ -86,4 +104,4 @@ func main() { skel.PluginMain(c, c) } ` const git_ref_v010 = "2c482f4" -const git_ref_v020 = "349d66d" +const git_ref_v020_no_custom_versions = "349d66d" diff --git a/skel/skel_test.go b/skel/skel_test.go index 0431abbb..1cc533bd 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -186,7 +186,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.3.0", + "cniVersion": "0.2.0", "supportedVersions": ["9.8.7"] }`)) }) diff --git a/version/plugin_test.go b/version/plugin_test.go index a58bd35a..124288fd 100644 --- a/version/plugin_test.go +++ b/version/plugin_test.go @@ -21,17 +21,21 @@ import ( ) var _ = Describe("Decoding versions reported by a plugin", func() { - var decoder *version.PluginDecoder + var ( + decoder *version.PluginDecoder + versionStdout []byte + ) BeforeEach(func() { decoder = &version.PluginDecoder{} + versionStdout = []byte(`{ + "cniVersion": "some-library-version", + "supportedVersions": [ "some-version", "some-other-version" ] + }`) }) It("returns a PluginInfo that represents the given json bytes", func() { - pluginInfo, err := decoder.Decode([]byte(`{ - "cniVersion": "some-library-version", - "supportedVersions": [ "some-version", "some-other-version" ] - }`)) + pluginInfo, err := decoder.Decode(versionStdout) Expect(err).NotTo(HaveOccurred()) Expect(pluginInfo).NotTo(BeNil()) Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ @@ -41,37 +45,40 @@ var _ = Describe("Decoding versions reported by a plugin", func() { }) Context("when the bytes cannot be decoded as json", func() { + BeforeEach(func() { + versionStdout = []byte(`{{{`) + }) + It("returns a meaningful error", func() { - _, err := decoder.Decode([]byte(`{{{`)) + _, err := decoder.Decode(versionStdout) Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string")) }) }) Context("when the json bytes are missing the required CNIVersion field", func() { + BeforeEach(func() { + versionStdout = []byte(`{ "supportedVersions": [ "foo" ] }`) + }) + It("returns a meaningful error", func() { - _, err := decoder.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`)) + _, err := decoder.Decode(versionStdout) Expect(err).To(MatchError("decoding version info: missing field cniVersion")) }) }) Context("when there are no supported versions", func() { - Context("when the cniVersion is 0.2.0", func() { - It("infers the supported versions are 0.1.0 and 0.2.0", func() { - pluginInfo, err := decoder.Decode([]byte(`{ "cniVersion": "0.2.0" }`)) - Expect(err).NotTo(HaveOccurred()) - Expect(pluginInfo).NotTo(BeNil()) - Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ - "0.1.0", - "0.2.0", - })) - }) + BeforeEach(func() { + versionStdout = []byte(`{ "cniVersion": "0.2.0" }`) }) - Context("when the cniVersion is >= 0.3.0", func() { - It("returns a meaningful error", func() { - _, err := decoder.Decode([]byte(`{ "cniVersion": "0.3.0" }`)) - Expect(err).To(MatchError("decoding version info: missing field supportedVersions")) - }) + It("assumes that the supported versions are 0.1.0 and 0.2.0", func() { + pluginInfo, err := decoder.Decode(versionStdout) + Expect(err).NotTo(HaveOccurred()) + Expect(pluginInfo).NotTo(BeNil()) + Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ + "0.1.0", + "0.2.0", + })) }) }) diff --git a/version/version.go b/version/version.go index 5f937f7e..e39c3b55 100644 --- a/version/version.go +++ b/version/version.go @@ -16,7 +16,7 @@ package version // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.3.0" + return "0.2.0" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -24,6 +24,6 @@ func Current() string { // library ought to work correctly with a plugin that reports support for // Legacy versions. // -// Any future CNI spec versions which meet this definition will be added to +// Any future CNI spec versions which meet this definition should be added to // this list. -var Legacy = PluginSupports("0.1.0", "0.2.0", "0.3.0") +var Legacy = PluginSupports("0.1.0", "0.2.0") From 921292ff77ca7519eb3a1f9d396aa4fc270c9027 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Tue, 6 Sep 2016 20:19:26 -0400 Subject: [PATCH 100/134] skel: Plugins require a cniVersion in the NetConf --- invoke/exec.go | 4 +++- invoke/exec_test.go | 7 ++++--- invoke/raw_exec_test.go | 2 +- skel/skel.go | 17 +++++++++++++++++ skel/skel_test.go | 26 +++++++++++++++++++++----- version/plugin.go | 1 + 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/invoke/exec.go b/invoke/exec.go index 7eb06156..167d38f5 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -16,6 +16,7 @@ package invoke import ( "encoding/json" + "fmt" "os" "github.com/containernetworking/cni/pkg/types" @@ -77,7 +78,8 @@ func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, erro IfName: "dummy", Path: "dummy", } - stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv()) + stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current())) + stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, stdin, args.AsEnv()) if err != nil { if err.Error() == "unknown CNI_COMMAND: VERSION" { return version.PluginSupports("0.1.0"), nil diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 94007d7c..2b9c9bf9 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -15,6 +15,7 @@ package invoke_test import ( + "encoding/json" "errors" "github.com/containernetworking/cni/pkg/invoke" @@ -48,7 +49,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { VersionDecoder: versionDecoder, } pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin" }`) + netconf = []byte(`{ "some": "stdin", "cniVersion": "0.2.0" }`) cniargs = &fakes.CNIArgs{} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) @@ -105,8 +106,9 @@ var _ = Describe("Executing a plugin, unit tests", func() { It("execs the plugin with the command VERSION", func() { pluginExec.GetVersionInfo(pluginPath) Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) - Expect(rawExec.ExecPluginCall.Received.StdinData).To(BeNil()) Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION")) + expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()}) + Expect(rawExec.ExecPluginCall.Received.StdinData).To(MatchJSON(expectedStdin)) }) It("decodes and returns the version info", func() { @@ -146,6 +148,5 @@ var _ = Describe("Executing a plugin, unit tests", func() { Expect(env).To(ContainElement("CNI_PATH=dummy")) }) }) - }) }) diff --git a/invoke/raw_exec_test.go b/invoke/raw_exec_test.go index 7df60a11..b0ca9607 100644 --- a/invoke/raw_exec_test.go +++ b/invoke/raw_exec_test.go @@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() { "CNI_PATH=/some/bin/path", "CNI_IFNAME=some-eth0", } - stdin = []byte(`{"some":"stdin-json"}`) + stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.2.0"}`) execer = &invoke.RawExec{} }) diff --git a/skel/skel.go b/skel/skel.go index de64d7dd..180ce24e 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -17,6 +17,7 @@ package skel import ( + "encoding/json" "fmt" "io" "io/ioutil" @@ -143,12 +144,28 @@ func createTypedError(f string, args ...interface{}) *types.Error { } } +func (t *dispatcher) validateVersion(stdinData []byte) error { + var netconf types.NetConf + if err := json.Unmarshal(stdinData, &netconf); err != nil { + return err + } + if netconf.CNIVersion == "" { + return fmt.Errorf("missing required config cniVersion") + } + + return nil +} + func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { cmd, cmdArgs, err := t.getCmdArgsFromEnv() if err != nil { return createTypedError(err.Error()) } + if err = t.validateVersion(cmdArgs.StdinData); err != nil { + return createTypedError(err.Error()) + } + switch cmd { case "ADD": err = cmdAdd(cmdArgs) diff --git a/skel/skel_test.go b/skel/skel_test.go index 1cc533bd..7ae25d35 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -17,7 +17,6 @@ package skel import ( "bytes" "errors" - "io" "strings" "github.com/containernetworking/cni/pkg/types" @@ -48,7 +47,7 @@ func (c *fakeCmd) Func(args *CmdArgs) error { var _ = Describe("dispatching to the correct callback", func() { var ( environment map[string]string - stdin io.Reader + stdinData string stdout, stderr *bytes.Buffer cmdAdd, cmdDel *fakeCmd dispatch *dispatcher @@ -65,13 +64,14 @@ var _ = Describe("dispatching to the correct callback", func() { "CNI_ARGS": "some;extra;args", "CNI_PATH": "/some/cni/path", } - stdin = strings.NewReader(`{ "some": "config" }`) + + stdinData = `{ "some": "config", "cniVersion": "9.8.7" }` stdout = &bytes.Buffer{} stderr = &bytes.Buffer{} versionInfo = version.PluginSupports("9.8.7") dispatch = &dispatcher{ Getenv: func(key string) string { return environment[key] }, - Stdin: stdin, + Stdin: strings.NewReader(stdinData), Stdout: stdout, Stderr: stderr, } @@ -83,7 +83,7 @@ var _ = Describe("dispatching to the correct callback", func() { IfName: "eth0", Args: "some;extra;args", Path: "/some/cni/path", - StdinData: []byte(`{ "some": "config" }`), + StdinData: []byte(stdinData), } }) @@ -144,6 +144,22 @@ var _ = Describe("dispatching to the correct callback", func() { }) }) + + Context("when the stdin data is missing the required cniVersion config", func() { + BeforeEach(func() { + dispatch.Stdin = strings.NewReader(`{ "some": "config" }`) + }) + + It("immediately returns a useful error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) + Expect(err).To(MatchError("missing required config cniVersion")) + }) + + It("does not call either callback", func() { + Expect(cmdAdd.CallCount).To(Equal(0)) + Expect(cmdDel.CallCount).To(Equal(0)) + }) + }) }) Context("when the CNI_COMMAND is DEL", func() { diff --git a/version/plugin.go b/version/plugin.go index 9bd7dc83..3a42fe25 100644 --- a/version/plugin.go +++ b/version/plugin.go @@ -56,6 +56,7 @@ func PluginSupports(supportedVersions ...string) PluginInfo { } } +// PluginDecoder can decode the response returned by a plugin's VERSION command type PluginDecoder struct{} func (_ *PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { From 1e4e8fdb5d6d7b31813bfd066a6c74f83f54e76a Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 18 Sep 2016 21:56:17 -0700 Subject: [PATCH 101/134] versioning: add basic version decode for network config --- version/conf.go | 37 ++++++++++++++++++++++++ version/conf_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 version/conf.go create mode 100644 version/conf_test.go diff --git a/version/conf.go b/version/conf.go new file mode 100644 index 00000000..3cca58bb --- /dev/null +++ b/version/conf.go @@ -0,0 +1,37 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "encoding/json" + "fmt" +) + +// ConfigDecoder can decode the CNI version available in network config data +type ConfigDecoder struct{} + +func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) { + var conf struct { + CNIVersion string `json:"cniVersion"` + } + err := json.Unmarshal(jsonBytes, &conf) + if err != nil { + return "", fmt.Errorf("decoding version from network config: %s", err) + } + if conf.CNIVersion == "" { + return "0.1.0", nil + } + return conf.CNIVersion, nil +} diff --git a/version/conf_test.go b/version/conf_test.go new file mode 100644 index 00000000..881c57ad --- /dev/null +++ b/version/conf_test.go @@ -0,0 +1,69 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version_test + +import ( + "github.com/containernetworking/cni/pkg/version" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Decoding the version of network config", func() { + var ( + decoder *version.ConfigDecoder + configBytes []byte + ) + + BeforeEach(func() { + decoder = &version.ConfigDecoder{} + configBytes = []byte(`{ "cniVersion": "4.3.2" }`) + }) + + Context("when the version is explict", func() { + It("returns the version", func() { + version, err := decoder.Decode(configBytes) + Expect(err).NotTo(HaveOccurred()) + + Expect(version).To(Equal("4.3.2")) + }) + }) + + Context("when the version is not present in the config", func() { + BeforeEach(func() { + configBytes = []byte(`{ "not-a-version-field": "foo" }`) + }) + + It("assumes the config is version 0.1.0", func() { + version, err := decoder.Decode(configBytes) + Expect(err).NotTo(HaveOccurred()) + + Expect(version).To(Equal("0.1.0")) + }) + }) + + Context("when the config data is malformed", func() { + BeforeEach(func() { + configBytes = []byte(`{{{`) + }) + + It("returns a useful error", func() { + _, err := decoder.Decode(configBytes) + Expect(err).To(MatchError(HavePrefix( + "decoding version from network config: invalid character", + ))) + }) + }) +}) From 04ba7c77615e1bd746e3b23959c72b1fb189f245 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Mon, 19 Sep 2016 13:00:49 -0700 Subject: [PATCH 102/134] versioning: plugins require version match with config infer version 0.1.0 when config is missing an explicit "cniVersion" field --- skel/skel.go | 33 ++++++++++++------------- skel/skel_test.go | 52 ++++++++++++++++++++++++++++++++++----- types/types.go | 8 ++++++ version/plugin.go | 2 +- version/reconcile.go | 47 +++++++++++++++++++++++++++++++++++ version/reconcile_test.go | 51 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 version/reconcile.go create mode 100644 version/reconcile_test.go diff --git a/skel/skel.go b/skel/skel.go index 180ce24e..8fc9636e 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -17,7 +17,6 @@ package skel import ( - "encoding/json" "fmt" "io" "io/ioutil" @@ -44,6 +43,9 @@ type dispatcher struct { Stdin io.Reader Stdout io.Writer Stderr io.Writer + + ConfVersionDecoder version.ConfigDecoder + VersionReconciler version.Reconciler } type reqForCmdEntry map[string]bool @@ -144,16 +146,20 @@ func createTypedError(f string, args ...interface{}) *types.Error { } } -func (t *dispatcher) validateVersion(stdinData []byte) error { - var netconf types.NetConf - if err := json.Unmarshal(stdinData, &netconf); err != nil { +func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error { + configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) + if err != nil { return err } - if netconf.CNIVersion == "" { - return fmt.Errorf("missing required config cniVersion") + verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) + if verErr != nil { + return &types.Error{ + Code: types.ErrIncompatibleCNIVersion, + Msg: "incompatible CNI versions", + Details: verErr.Details(), + } } - - return nil + return toCall(cmdArgs) } func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { @@ -162,20 +168,13 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionIn return createTypedError(err.Error()) } - if err = t.validateVersion(cmdArgs.StdinData); err != nil { - return createTypedError(err.Error()) - } - switch cmd { case "ADD": - err = cmdAdd(cmdArgs) - + err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd) case "DEL": - err = cmdDel(cmdArgs) - + err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel) case "VERSION": err = versionInfo.Encode(t.Stdout) - default: return createTypedError("unknown CNI_COMMAND: %v", cmd) } diff --git a/skel/skel_test.go b/skel/skel_test.go index 7ae25d35..570f0272 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -150,14 +150,38 @@ var _ = Describe("dispatching to the correct callback", func() { dispatch.Stdin = strings.NewReader(`{ "some": "config" }`) }) - It("immediately returns a useful error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - Expect(err).To(MatchError("missing required config cniVersion")) + Context("when the plugin supports version 0.1.0", func() { + BeforeEach(func() { + versionInfo = version.PluginSupports("0.1.0") + expectedCmdArgs.StdinData = []byte(`{ "some": "config" }`) + }) + + It("infers the config is 0.1.0 and calls the cmdAdd callback", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) + Expect(err).NotTo(HaveOccurred()) + + Expect(cmdAdd.CallCount).To(Equal(1)) + Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) + }) }) - It("does not call either callback", func() { - Expect(cmdAdd.CallCount).To(Equal(0)) - Expect(cmdDel.CallCount).To(Equal(0)) + Context("when the plugin does not support 0.1.0", func() { + BeforeEach(func() { + versionInfo = version.PluginSupports("4.3.2") + }) + + It("immediately returns a useful error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) + Expect(err.Code).To(Equal(uint(1))) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes + Expect(err.Msg).To(Equal("incompatible CNI versions")) + Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`)) + }) + + It("does not call either callback", func() { + dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) + Expect(cmdAdd.CallCount).To(Equal(0)) + Expect(cmdDel.CallCount).To(Equal(0)) + }) }) }) }) @@ -223,6 +247,22 @@ var _ = Describe("dispatching to the correct callback", func() { Entry("args", "CNI_ARGS", false), Entry("path", "CNI_PATH", false), ) + + Context("when the stdin is empty", func() { + BeforeEach(func() { + dispatch.Stdin = strings.NewReader("") + }) + + It("succeeds without error", func() { + err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) + + Expect(err).NotTo(HaveOccurred()) + Expect(stdout).To(MatchJSON(`{ + "cniVersion": "0.2.0", + "supportedVersions": ["9.8.7"] + }`)) + }) + }) }) Context("when the CNI_COMMAND is unrecognized", func() { diff --git a/types/types.go b/types/types.go index ba02580a..17caa49b 100644 --- a/types/types.go +++ b/types/types.go @@ -112,6 +112,14 @@ type Route struct { GW net.IP } +// Well known error codes +// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes +const ( + ErrUnknown uint = iota // 0 + ErrIncompatibleCNIVersion // 1 + ErrUnsupportedField // 2 +) + type Error struct { Code uint `json:"code"` Msg string `json:"msg"` diff --git a/version/plugin.go b/version/plugin.go index 3a42fe25..dc937b54 100644 --- a/version/plugin.go +++ b/version/plugin.go @@ -59,7 +59,7 @@ func PluginSupports(supportedVersions ...string) PluginInfo { // PluginDecoder can decode the response returned by a plugin's VERSION command type PluginDecoder struct{} -func (_ *PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { +func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { var info pluginInfo err := json.Unmarshal(jsonBytes, &info) if err != nil { diff --git a/version/reconcile.go b/version/reconcile.go new file mode 100644 index 00000000..f61ef653 --- /dev/null +++ b/version/reconcile.go @@ -0,0 +1,47 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import "fmt" + +type ErrorIncompatible struct { + Config string + Plugin []string +} + +func (e *ErrorIncompatible) Details() string { + return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Plugin) +} + +func (e *ErrorIncompatible) Error() string { + return fmt.Sprintf("incompatible CNI versions: %s", e.Details()) +} + +type Reconciler struct{} + +func (*Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { + pluginVersions := pluginInfo.SupportedVersions() + + for _, pluginVersion := range pluginVersions { + if configVersion == pluginVersion { + return nil + } + } + + return &ErrorIncompatible{ + Config: configVersion, + Plugin: pluginVersions, + } +} diff --git a/version/reconcile_test.go b/version/reconcile_test.go new file mode 100644 index 00000000..19a9e23f --- /dev/null +++ b/version/reconcile_test.go @@ -0,0 +1,51 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version_test + +import ( + "github.com/containernetworking/cni/pkg/version" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() { + var ( + reconciler *version.Reconciler + pluginInfo version.PluginInfo + ) + + BeforeEach(func() { + reconciler = &version.Reconciler{} + pluginInfo = version.PluginSupports("1.2.3", "4.3.2") + }) + + It("succeeds if the config version is supported by the plugin", func() { + err := reconciler.Check("4.3.2", pluginInfo) + Expect(err).NotTo(HaveOccurred()) + }) + + Context("when the config version is not supported by the plugin", func() { + It("returns a helpful error", func() { + err := reconciler.Check("0.1.0", pluginInfo) + + Expect(err).To(Equal(&version.ErrorIncompatible{ + Config: "0.1.0", + Plugin: []string{"1.2.3", "4.3.2"}, + })) + + Expect(err.Error()).To(Equal(`incompatible CNI versions: config is "0.1.0", plugin supports ["1.2.3" "4.3.2"]`)) + }) + }) +}) From f721eaf6edc82e9273aadfdc29dc3828d762974c Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Mon, 19 Sep 2016 13:14:02 -0700 Subject: [PATCH 103/134] skel: use named constant for Incompatible CNI Version error code --- skel/skel_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skel/skel_test.go b/skel/skel_test.go index 570f0272..cd668c91 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -172,7 +172,7 @@ var _ = Describe("dispatching to the correct callback", func() { It("immediately returns a useful error", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - Expect(err.Code).To(Equal(uint(1))) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes + Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes Expect(err.Msg).To(Equal("incompatible CNI versions")) Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`)) }) From b07b226c97088f8feddf5327a136dfda6cc77678 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 2 Oct 2016 21:43:51 -0700 Subject: [PATCH 104/134] testing: adds basic test of backwards compatibility --- version/legacy_examples/examples.go | 138 ++++++++++++++++++ .../legacy_examples_suite_test.go | 27 ++++ .../legacy_examples/legacy_examples_test.go | 36 +++++ 3 files changed, 201 insertions(+) create mode 100644 version/legacy_examples/examples.go create mode 100644 version/legacy_examples/legacy_examples_suite_test.go create mode 100644 version/legacy_examples/legacy_examples_test.go diff --git a/version/legacy_examples/examples.go b/version/legacy_examples/examples.go new file mode 100644 index 00000000..57162312 --- /dev/null +++ b/version/legacy_examples/examples.go @@ -0,0 +1,138 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package legacy_examples contains sample code from prior versions of +// the CNI library, for use in verifying backwards compatibility. +package legacy_examples + +import ( + "io/ioutil" + "net" + "path/filepath" + "sync" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version/testhelpers" +) + +// An Example is a Git reference to the CNI repo and a Golang CNI plugin that +// builds against that version of the repo. +// +// By convention, every Example plugin returns an ADD result that is +// semantically equivalent to the ExpectedResult. +type Example struct { + Name string + CNIRepoGitRef string + PluginSource string +} + +var buildDir = "" +var buildDirLock sync.Mutex + +func ensureBuildDirExists() error { + buildDirLock.Lock() + defer buildDirLock.Unlock() + + if buildDir != "" { + return nil + } + + var err error + buildDir, err = ioutil.TempDir("", "cni-example-plugins") + return err +} + +// Build builds the example, returning the path to the binary +func (e Example) Build() (string, error) { + if err := ensureBuildDirExists(); err != nil { + return "", err + } + + outBinPath := filepath.Join(buildDir, e.Name) + + if err := testhelpers.BuildAt([]byte(e.PluginSource), e.CNIRepoGitRef, outBinPath); err != nil { + return "", err + } + return outBinPath, nil +} + +// V010 acts like a CNI plugin from the v0.1.0 era +var V010 = Example{ + Name: "example_v010", + CNIRepoGitRef: "2c482f4", + PluginSource: `package main + +import ( + "net" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" +) + +var result = types.Result{ + IP4: &types.IPConfig{ + IP: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.CIDRMask(24, 32), + }, + Gateway: net.ParseIP("10.1.2.1"), + Routes: []types.Route{ + types.Route{ + Dst: net.IPNet{ + IP: net.ParseIP("0.0.0.0"), + Mask: net.CIDRMask(0, 32), + }, + GW: net.ParseIP("10.1.0.1"), + }, + }, + }, + DNS: types.DNS{ + Nameservers: []string{"8.8.8.8"}, + Domain: "example.com", + }, +} + +func c(_ *skel.CmdArgs) error { result.Print(); return nil } + +func main() { skel.PluginMain(c, c) } +`, +} + +// ExpectedResult is the current representation of the plugin result +// that is expected from each of the examples. +// +// As we change the CNI spec, the Result type and this value may change. +// The text of the example plugins should not. +var ExpectedResult = &types.Result{ + IP4: &types.IPConfig{ + IP: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.CIDRMask(24, 32), + }, + Gateway: net.ParseIP("10.1.2.1"), + Routes: []types.Route{ + types.Route{ + Dst: net.IPNet{ + IP: net.ParseIP("0.0.0.0"), + Mask: net.CIDRMask(0, 32), + }, + GW: net.ParseIP("10.1.0.1"), + }, + }, + }, + DNS: types.DNS{ + Nameservers: []string{"8.8.8.8"}, + Domain: "example.com", + }, +} diff --git a/version/legacy_examples/legacy_examples_suite_test.go b/version/legacy_examples/legacy_examples_suite_test.go new file mode 100644 index 00000000..a126531d --- /dev/null +++ b/version/legacy_examples/legacy_examples_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy_examples_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestLegacyExamples(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "LegacyExamples Suite") +} diff --git a/version/legacy_examples/legacy_examples_test.go b/version/legacy_examples/legacy_examples_test.go new file mode 100644 index 00000000..41151056 --- /dev/null +++ b/version/legacy_examples/legacy_examples_test.go @@ -0,0 +1,36 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy_examples_test + +import ( + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/version/legacy_examples" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("The v0.1.0 Example", func() { + It("builds ok", func() { + example := legacy_examples.V010 + pluginPath, err := example.Build() + Expect(err).NotTo(HaveOccurred()) + + Expect(filepath.Base(pluginPath)).To(Equal(example.Name)) + + Expect(os.RemoveAll(pluginPath)).To(Succeed()) + }) +}) From cfb2ff3dcd56a6806d297a3905de5c828a57aabb Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 16 Oct 2016 17:18:57 -0700 Subject: [PATCH 105/134] skel tests: correct name of CNI_CONTAINERID in tests of env vars --- skel/skel_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skel/skel_test.go b/skel/skel_test.go index cd668c91..6652fcdb 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -121,7 +121,7 @@ var _ = Describe("dispatching to the correct callback", func() { DescribeTable("required / optional env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINER_ID", false), + Entry("container id", "CNI_CONTAINERID", false), Entry("net ns", "CNI_NETNS", true), Entry("if name", "CNI_IFNAME", true), Entry("args", "CNI_ARGS", false), @@ -208,7 +208,7 @@ var _ = Describe("dispatching to the correct callback", func() { DescribeTable("required / optional env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINER_ID", false), + Entry("container id", "CNI_CONTAINERID", false), Entry("net ns", "CNI_NETNS", false), Entry("if name", "CNI_IFNAME", true), Entry("args", "CNI_ARGS", false), @@ -241,7 +241,7 @@ var _ = Describe("dispatching to the correct callback", func() { DescribeTable("VERSION does not need the usual env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINER_ID", false), + Entry("container id", "CNI_CONTAINERID", false), Entry("net ns", "CNI_NETNS", false), Entry("if name", "CNI_IFNAME", false), Entry("args", "CNI_ARGS", false), From 61d3f4515a6d17f69f71c031d3a5fd7ab22d7302 Mon Sep 17 00:00:00 2001 From: Casey Callendrello Date: Fri, 14 Oct 2016 16:38:32 +0200 Subject: [PATCH 106/134] testing: test invocation of newer plugins with an older libcni --- version/legacy_examples/example_runtime.go | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 version/legacy_examples/example_runtime.go diff --git a/version/legacy_examples/example_runtime.go b/version/legacy_examples/example_runtime.go new file mode 100644 index 00000000..a461981f --- /dev/null +++ b/version/legacy_examples/example_runtime.go @@ -0,0 +1,167 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy_examples + +// An ExampleRuntime is a small program that uses libcni to invoke a network plugin. +// It should call ADD and DELETE, verifying all intermediate steps +// and data structures. +type ExampleRuntime struct { + Example + NetConfs []string // The network configuration names to pass +} + +// NetConfs are various versioned network configuration files. Examples should +// specify which version they expect +var NetConfs = map[string]string{ + "unversioned": `{ + "name": "default", + "type": "ptp", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, + "0.1.0": `{ + "cniVersion": "0.1.0", + "name": "default", + "type": "ptp", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, +} + +// V010_Runtime creates a simple ptp network configuration, then +// executes libcni against the currently-built plugins. +var V010_Runtime = ExampleRuntime{ + NetConfs: []string{"unversioned", "0.1.0"}, + Example: Example{ + Name: "example_invoker_v010", + CNIRepoGitRef: "c0d34c69", //version with ns.Do + PluginSource: `package main + +import ( + "fmt" + "io/ioutil" + "net" + "os" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/libcni" +) + +func main(){ + code := exec() + os.Exit(code) +} + +func exec() int { + confBytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Printf("could not read netconfig from stdin: %+v", err) + return 1 + } + + netConf, err := libcni.ConfFromBytes(confBytes) + if err != nil { + fmt.Printf("could not parse netconfig: %+v", err) + return 1 + } + fmt.Printf("Parsed network configuration: %+v\n", netConf.Network) + + if len(os.Args) == 1 { + fmt.Printf("Expect CNI plugin paths in argv") + return 1 + } + + targetNs, err := ns.NewNS() + if err != nil { + fmt.Printf("Could not create ns: %+v", err) + return 1 + } + defer targetNs.Close() + + ifName := "eth0" + + runtimeConf := &libcni.RuntimeConf{ + ContainerID: "some-container-id", + NetNS: targetNs.Path(), + IfName: ifName, + } + + cniConfig := &libcni.CNIConfig{Path: os.Args[1:]} + + result, err := cniConfig.AddNetwork(netConf, runtimeConf) + if err != nil { + fmt.Printf("AddNetwork failed: %+v", err) + return 2 + } + fmt.Printf("AddNetwork result: %+v", result) + + expectedIP := result.IP4.IP + + err = targetNs.Do(func(ns.NetNS) error { + netif, err := net.InterfaceByName(ifName) + if err != nil { + return fmt.Errorf("could not retrieve interface: %v", err) + } + + addrs, err := netif.Addrs() + if err != nil { + return fmt.Errorf("could not retrieve addresses, %+v", err) + } + + found := false + for _, addr := range addrs { + if addr.String() == expectedIP.String() { + found = true + break + } + } + + if !found { + return fmt.Errorf("Far-side link did not have expected address %s", expectedIP) + } + return nil + }) + if err != nil { + fmt.Println(err) + return 4 + } + + err = cniConfig.DelNetwork(netConf, runtimeConf) + if err != nil { + fmt.Printf("DelNetwork failed: %v", err) + return 5 + } + + err = targetNs.Do(func(ns.NetNS) error { + _, err := net.InterfaceByName(ifName) + if err == nil { + return fmt.Errorf("interface was not deleted") + } + return nil + }) + if err != nil { + fmt.Println(err) + return 6 + } + + return 0 +} +`, + }, +} From 44ef10948340e07f4b58a5443a881b4b0c69a5da Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 17 Nov 2016 15:45:38 +0000 Subject: [PATCH 107/134] Expand description of goroutine+netns problems --- ns/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ns/README.md b/ns/README.md index e7b20c2f..99aed9c8 100644 --- a/ns/README.md +++ b/ns/README.md @@ -9,7 +9,7 @@ Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly. ### Do() The Recommended Thing -The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example: +The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace (including the root namespace) should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example: ```go targetNs, err := ns.NewNS() @@ -26,6 +26,9 @@ err = targetNs.Do(func(hostNs ns.NetNS) error { }) ``` +Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem. + ### Further Reading - https://github.com/golang/go/wiki/LockOSThread - http://morsmachine.dk/go-scheduler + - https://github.com/containernetworking/cni/issues/262 From 06fd1369aabb041b1743146ded582ace8a459b21 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 14 Dec 2016 17:09:01 -0600 Subject: [PATCH 108/134] all: assert internal objects implement interfaces --- invoke/args.go | 3 +++ ns/ns.go | 3 +++ version/plugin.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/invoke/args.go b/invoke/args.go index be28ba62..ba9d0c3b 100644 --- a/invoke/args.go +++ b/invoke/args.go @@ -47,6 +47,9 @@ type Args struct { Path string } +// Args implements the CNIArgs interface +var _ CNIArgs = &Args{} + func (args *Args) AsEnv() []string { env := os.Environ() pluginArgsStr := args.PluginArgsStr diff --git a/ns/ns.go b/ns/ns.go index 3246ebf3..220dd694 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -62,6 +62,9 @@ type netNS struct { closed bool } +// netNS implements the NetNS interface +var _ NetNS = &netNS{} + func getCurrentThreadNetNSPath() string { // /proc/self/ns/net returns the namespace of the main thread, not // of whatever thread this goroutine is running on. Make sure we diff --git a/version/plugin.go b/version/plugin.go index dc937b54..8a467281 100644 --- a/version/plugin.go +++ b/version/plugin.go @@ -36,6 +36,9 @@ type pluginInfo struct { SupportedVersions_ []string `json:"supportedVersions,omitempty"` } +// pluginInfo implements the PluginInfo interface +var _ PluginInfo = &pluginInfo{} + func (p *pluginInfo) Encode(w io.Writer) error { return json.NewEncoder(w).Encode(p) } From 13534e505c6b7d1904e27a199625d9a50c5ef97e Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Fri, 16 Dec 2016 18:56:39 -0600 Subject: [PATCH 109/134] api,libcni: add network config list-based plugin chaining Using a new ".configlist" file format that allows specifying a list of CNI network configurations to run, add new libcni helper functions to call each plugin in the list, injecting the overall name, CNI version, and previous plugin's Result structure into the configuration of the next plugin. --- types/types.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/types/types.go b/types/types.go index 17caa49b..c1fddcd5 100644 --- a/types/types.go +++ b/types/types.go @@ -59,14 +59,23 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - IPAM struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + PrevResult *Result `json:"prevResult,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"` } +// NetConfList describes an ordered list of networks. +type NetConfList struct { + CNIVersion string `json:"cniVersion,omitempty"` + + Name string `json:"name,omitempty"` + Plugins []*NetConf `json:"plugins,omitempty"` +} + // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { IP4 *IPConfig `json:"ip4,omitempty"` From 2515b8c71283f070e5517a3cbffc243cf9845c86 Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Sun, 22 Jan 2017 16:39:15 -0800 Subject: [PATCH 110/134] skel: adds PluginMainWithError which returns a *types.Error Enables plugins to do their own error-handling and cleanup logic --- skel/skel.go | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/skel/skel.go b/skel/skel.go index 8fc9636e..8644c25e 100644 --- a/skel/skel.go +++ b/skel/skel.go @@ -189,25 +189,40 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionIn return nil } -// PluginMain is the "main" for a plugin. It accepts -// two callback functions for add and del commands. -func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) { - caller := dispatcher{ +// PluginMainWithError is the core "main" for a plugin. It accepts +// callback functions for add and del CNI commands and returns an error. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// It is the responsibility of the caller to check for non-nil error return. +// +// For a plugin to comply with the CNI spec, it must print any error to stdout +// as JSON and then exit with nonzero status code. +// +// To let this package automatically handle errors and call os.Exit(1) for you, +// use PluginMain() instead. +func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { + return (&dispatcher{ Getenv: os.Getenv, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, - } - - err := caller.pluginMain(cmdAdd, cmdDel, versionInfo) - if err != nil { - dieErr(err) - } + }).pluginMain(cmdAdd, cmdDel, versionInfo) } -func dieErr(e *types.Error) { - if err := e.Print(); err != nil { - log.Print("Error writing error JSON to stdout: ", err) +// PluginMain is the core "main" for a plugin which includes automatic error handling. +// +// The caller must also specify what CNI spec versions the plugin supports. +// +// When an error occurs in either cmdAdd or cmdDel, PluginMain will print the error +// as JSON to stdout and call os.Exit(1). +// +// To have more control over error handling, use PluginMainWithError() instead. +func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) { + if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil { + if err := e.Print(); err != nil { + log.Print("Error writing error JSON to stdout: ", err) + } + os.Exit(1) } - os.Exit(1) } From dae1177b531f80b41e82ab6eb9b00766e7c49188 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 22 Nov 2016 10:02:39 -0600 Subject: [PATCH 111/134] testutils: pass netConf in for version operations; pass raw result out for tests --- testutils/cmd.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testutils/cmd.go b/testutils/cmd.go index 201b935f..0118f61c 100644 --- a/testutils/cmd.go +++ b/testutils/cmd.go @@ -29,7 +29,7 @@ func envCleanup() { os.Unsetenv("CNI_IFNAME") } -func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result, error) { +func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (*types.Result, []byte, error) { os.Setenv("CNI_COMMAND", "ADD") os.Setenv("CNI_PATH", os.Getenv("PATH")) os.Setenv("CNI_NETNS", cniNetns) @@ -40,30 +40,30 @@ func CmdAddWithResult(cniNetns, cniIfname string, f func() error) (*types.Result oldStdout := os.Stdout r, w, err := os.Pipe() if err != nil { - return nil, err + return nil, nil, err } os.Stdout = w err = f() w.Close() if err != nil { - return nil, err + return nil, nil, err } // parse the result out, err := ioutil.ReadAll(r) os.Stdout = oldStdout if err != nil { - return nil, err + return nil, nil, err } result := types.Result{} err = json.Unmarshal(out, &result) if err != nil { - return nil, err + return nil, nil, err } - return &result, nil + return &result, out, nil } func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { From 0d19b79260f4fca812daab6b3ff21c54fc12cfb8 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 9 Nov 2016 15:11:18 -0600 Subject: [PATCH 112/134] types: make Result an interface and move existing Result to separate package --- invoke/delegate.go | 2 +- invoke/exec.go | 16 +-- invoke/exec_test.go | 6 +- ipam/ipam.go | 5 +- testutils/cmd.go | 16 ++- types/current/types.go | 157 ++++++++++++++++++++++++++++ types/current/types_suite_test.go | 27 +++++ types/current/types_test.go | 128 +++++++++++++++++++++++ types/types.go | 85 +++++---------- version/legacy_examples/examples.go | 5 +- version/reconcile.go | 20 ++-- version/reconcile_test.go | 4 +- version/version.go | 29 +++++ 13 files changed, 412 insertions(+), 88 deletions(-) create mode 100644 types/current/types.go create mode 100644 types/current/types_suite_test.go create mode 100644 types/current/types_test.go diff --git a/invoke/delegate.go b/invoke/delegate.go index ddf1d172..f25beddc 100644 --- a/invoke/delegate.go +++ b/invoke/delegate.go @@ -22,7 +22,7 @@ import ( "github.com/containernetworking/cni/pkg/types" ) -func DelegateAdd(delegatePlugin string, netconf []byte) (*types.Result, error) { +func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) { if os.Getenv("CNI_COMMAND") != "ADD" { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } diff --git a/invoke/exec.go b/invoke/exec.go index 167d38f5..fc47e7c8 100644 --- a/invoke/exec.go +++ b/invoke/exec.go @@ -15,7 +15,6 @@ package invoke import ( - "encoding/json" "fmt" "os" @@ -23,7 +22,7 @@ import ( "github.com/containernetworking/cni/pkg/version" ) -func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { +func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { return defaultPluginExec.WithResult(pluginPath, netconf, args) } @@ -49,15 +48,20 @@ type PluginExec struct { } } -func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) { +func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) if err != nil { return nil, err } - res := &types.Result{} - err = json.Unmarshal(stdoutBytes, res) - return res, err + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(netconf) + if err != nil { + return nil, err + } + + return version.NewResult(confVersion, stdoutBytes) } func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 2b9c9bf9..3e207c14 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -20,6 +20,7 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/invoke/fakes" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" . "github.com/onsi/ginkgo" @@ -56,7 +57,10 @@ var _ = Describe("Executing a plugin, unit tests", func() { Describe("returning a result", func() { It("unmarshals the result bytes into the Result type", func() { - result, err := pluginExec.WithResult(pluginPath, netconf, cniargs) + r, err := pluginExec.WithResult(pluginPath, netconf, cniargs) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4")) }) diff --git a/ipam/ipam.go b/ipam/ipam.go index d9fbff74..8dd861a6 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -21,11 +21,12 @@ import ( "github.com/containernetworking/cni/pkg/invoke" "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/vishvananda/netlink" ) -func ExecAdd(plugin string, netconf []byte) (*types.Result, error) { +func ExecAdd(plugin string, netconf []byte) (types.Result, error) { return invoke.DelegateAdd(plugin, netconf) } @@ -35,7 +36,7 @@ func ExecDel(plugin string, netconf []byte) error { // ConfigureIface takes the result of IPAM plugin and // applies to the ifName interface -func ConfigureIface(ifName string, res *types.Result) error { +func ConfigureIface(ifName string, res *current.Result) error { link, err := netlink.LinkByName(ifName) if err != nil { return fmt.Errorf("failed to lookup %q: %v", ifName, err) diff --git a/testutils/cmd.go b/testutils/cmd.go index 0118f61c..5883c08f 100644 --- a/testutils/cmd.go +++ b/testutils/cmd.go @@ -15,11 +15,11 @@ package testutils import ( - "encoding/json" "io/ioutil" "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) func envCleanup() { @@ -29,7 +29,7 @@ func envCleanup() { os.Unsetenv("CNI_IFNAME") } -func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (*types.Result, []byte, error) { +func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) { os.Setenv("CNI_COMMAND", "ADD") os.Setenv("CNI_PATH", os.Getenv("PATH")) os.Setenv("CNI_NETNS", cniNetns) @@ -57,13 +57,19 @@ func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) ( return nil, nil, err } - result := types.Result{} - err = json.Unmarshal(out, &result) + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + confVersion, err := versionDecoder.Decode(conf) if err != nil { return nil, nil, err } - return &result, out, nil + result, err := version.NewResult(confVersion, out) + if err != nil { + return nil, nil, err + } + + return result, out, nil } func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { diff --git a/types/current/types.go b/types/current/types.go new file mode 100644 index 00000000..338b3fd2 --- /dev/null +++ b/types/current/types.go @@ -0,0 +1,157 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package current + +import ( + "encoding/json" + "fmt" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" +) + +const implementedSpecVersion string = "0.2.0" + +var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} + +func NewResult(data []byte) (types.Result, error) { + result := &Result{} + if err := json.Unmarshal(data, result); err != nil { + return nil, err + } + return result, nil +} + +func GetResult(r types.Result) (*Result, error) { + newResult, err := r.GetAsVersion(implementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := newResult.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return result, nil +} + +var resultConverters = []struct { + versions []string + convert func(types.Result) (*Result, error) +}{ + {SupportedVersions, convertFrom020}, +} + +func convertFrom020(result types.Result) (*Result, error) { + newResult, ok := result.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return newResult, nil +} + +func NewResultFromResult(result types.Result) (*Result, error) { + version := result.Version() + for _, converter := range resultConverters { + for _, supportedVersion := range converter.versions { + if version == supportedVersion { + return converter.convert(result) + } + } + } + return nil, fmt.Errorf("unsupported CNI result version %q", version) +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` + DNS types.DNS `json:"dns,omitempty"` +} + +func (r *Result) Version() string { + return implementedSpecVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + for _, supportedVersion := range SupportedVersions { + if version == supportedVersion { + return r, nil + } + } + return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) +} + +func (r *Result) Print() error { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(data) + return err +} + +// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where +// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the +// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. +func (r *Result) String() string { + var str string + if r.IP4 != nil { + str = fmt.Sprintf("IP4:%+v, ", *r.IP4) + } + if r.IP6 != nil { + str += fmt.Sprintf("IP6:%+v, ", *r.IP6) + } + return fmt.Sprintf("%sDNS:%+v", str, r.DNS) +} + +// IPConfig contains values necessary to configure an interface +type IPConfig struct { + IP net.IPNet + Gateway net.IP + Routes []types.Route +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP types.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []types.Route `json:"routes,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: types.IPNet(c.IP), + Gateway: c.Gateway, + Routes: c.Routes, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.IP = net.IPNet(ipc.IP) + c.Gateway = ipc.Gateway + c.Routes = ipc.Routes + return nil +} diff --git a/types/current/types_suite_test.go b/types/current/types_suite_test.go new file mode 100644 index 00000000..42a47a25 --- /dev/null +++ b/types/current/types_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package current_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes010(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "0.1.0 Types Suite") +} diff --git a/types/current/types_test.go b/types/current/types_test.go new file mode 100644 index 00000000..3810999d --- /dev/null +++ b/types/current/types_test.go @@ -0,0 +1,128 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package current_test + +import ( + "io/ioutil" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { + It("correctly encodes a 0.1.0 Result", func() { + ipv4, err := types.ParseCIDR("1.2.3.30/24") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv4).NotTo(BeNil()) + + routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") + Expect(err).NotTo(HaveOccurred()) + Expect(routev4).NotTo(BeNil()) + Expect(routegwv4).NotTo(BeNil()) + + ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv6).NotTo(BeNil()) + + routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") + Expect(err).NotTo(HaveOccurred()) + Expect(routev6).NotTo(BeNil()) + Expect(routegwv6).NotTo(BeNil()) + + // Set every field of the struct to ensure source compatibility + res := current.Result{ + IP4: ¤t.IPConfig{ + IP: *ipv4, + Gateway: net.ParseIP("1.2.3.1"), + Routes: []types.Route{ + {Dst: *routev4, GW: routegwv4}, + }, + }, + IP6: ¤t.IPConfig{ + IP: *ipv6, + Gateway: net.ParseIP("abcd:1234:ffff::1"), + Routes: []types.Route{ + {Dst: *routev6, GW: routegwv6}, + }, + }, + DNS: types.DNS{ + Nameservers: []string{"1.2.3.4", "1::cafe"}, + Domain: "acompany.com", + Search: []string{"somedomain.com", "otherdomain.net"}, + Options: []string{"foo", "bar"}, + }, + } + + Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) + + // Redirect stdout to capture JSON result + oldStdout := os.Stdout + r, w, err := os.Pipe() + Expect(err).NotTo(HaveOccurred()) + + os.Stdout = w + err = res.Print() + w.Close() + Expect(err).NotTo(HaveOccurred()) + + // parse the result + out, err := ioutil.ReadAll(r) + os.Stdout = oldStdout + Expect(err).NotTo(HaveOccurred()) + + Expect(string(out)).To(Equal(`{ + "ip4": { + "ip": "1.2.3.30/24", + "gateway": "1.2.3.1", + "routes": [ + { + "dst": "15.5.6.0/24", + "gw": "15.5.6.8" + } + ] + }, + "ip6": { + "ip": "abcd:1234:ffff::cdde/64", + "gateway": "abcd:1234:ffff::1", + "routes": [ + { + "dst": "1111:dddd::/80", + "gw": "1111:dddd::aaaa" + } + ] + }, + "dns": { + "nameservers": [ + "1.2.3.4", + "1::cafe" + ], + "domain": "acompany.com", + "search": [ + "somedomain.com", + "otherdomain.net" + ], + "options": [ + "foo", + "bar" + ] + } +}`)) + }) +}) diff --git a/types/types.go b/types/types.go index c1fddcd5..2ceffebc 100644 --- a/types/types.go +++ b/types/types.go @@ -16,7 +16,6 @@ package types import ( "encoding/json" - "fmt" "net" "os" ) @@ -59,10 +58,9 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - PrevResult *Result `json:"prevResult,omitempty"` - IPAM struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"` @@ -76,36 +74,31 @@ type NetConfList struct { Plugins []*NetConf `json:"plugins,omitempty"` } -// Result is what gets returned from the plugin (via stdout) to the caller -type Result struct { - IP4 *IPConfig `json:"ip4,omitempty"` - IP6 *IPConfig `json:"ip6,omitempty"` - DNS DNS `json:"dns,omitempty"` +type ResultFactoryFunc func([]byte) (Result, error) + +// Result is an interface that provides the result of plugin execution +type Result interface { + // The highest CNI specification result verison the result supports + // without having to convert + Version() string + + // Returns the result converted into the requested CNI specification + // result version, or an error if conversion failed + GetAsVersion(version string) (Result, error) + + // Prints the result in JSON format to stdout + Print() error + + // Returns a JSON string representation of the result + String() string } -func (r *Result) Print() error { - return prettyPrint(r) -} - -// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where -// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the -// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. -func (r *Result) String() string { - var str string - if r.IP4 != nil { - str = fmt.Sprintf("IP4:%+v, ", *r.IP4) +func PrintResult(result Result, version string) error { + newResult, err := result.GetAsVersion(version) + if err != nil { + return err } - if r.IP6 != nil { - str += fmt.Sprintf("IP6:%+v, ", *r.IP6) - } - return fmt.Sprintf("%sDNS:%+v", str, r.DNS) -} - -// IPConfig contains values necessary to configure an interface -type IPConfig struct { - IP net.IPNet - Gateway net.IP - Routes []Route + return newResult.Print() } // DNS contains values interesting for DNS resolvers @@ -147,39 +140,11 @@ func (e *Error) Print() error { // for our custom IPNet type // JSON (un)marshallable types -type ipConfig struct { - IP IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []Route `json:"routes,omitempty"` -} - type route struct { Dst IPNet `json:"dst"` GW net.IP `json:"gw,omitempty"` } -func (c *IPConfig) MarshalJSON() ([]byte, error) { - ipc := ipConfig{ - IP: IPNet(c.IP), - Gateway: c.Gateway, - Routes: c.Routes, - } - - return json.Marshal(ipc) -} - -func (c *IPConfig) UnmarshalJSON(data []byte) error { - ipc := ipConfig{} - if err := json.Unmarshal(data, &ipc); err != nil { - return err - } - - c.IP = net.IPNet(ipc.IP) - c.Gateway = ipc.Gateway - c.Routes = ipc.Routes - return nil -} - func (r *Route) UnmarshalJSON(data []byte) error { rt := route{} if err := json.Unmarshal(data, &rt); err != nil { diff --git a/version/legacy_examples/examples.go b/version/legacy_examples/examples.go index 57162312..8b079a3d 100644 --- a/version/legacy_examples/examples.go +++ b/version/legacy_examples/examples.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version/testhelpers" ) @@ -114,8 +115,8 @@ func main() { skel.PluginMain(c, c) } // // As we change the CNI spec, the Result type and this value may change. // The text of the example plugins should not. -var ExpectedResult = &types.Result{ - IP4: &types.IPConfig{ +var ExpectedResult = ¤t.Result{ + IP4: ¤t.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.CIDRMask(24, 32), diff --git a/version/reconcile.go b/version/reconcile.go index f61ef653..25c3810b 100644 --- a/version/reconcile.go +++ b/version/reconcile.go @@ -17,12 +17,12 @@ package version import "fmt" type ErrorIncompatible struct { - Config string - Plugin []string + Config string + Supported []string } func (e *ErrorIncompatible) Details() string { - return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Plugin) + return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Supported) } func (e *ErrorIncompatible) Error() string { @@ -31,17 +31,19 @@ func (e *ErrorIncompatible) Error() string { type Reconciler struct{} -func (*Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { - pluginVersions := pluginInfo.SupportedVersions() +func (r *Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { + return r.CheckRaw(configVersion, pluginInfo.SupportedVersions()) +} - for _, pluginVersion := range pluginVersions { - if configVersion == pluginVersion { +func (*Reconciler) CheckRaw(configVersion string, supportedVersions []string) *ErrorIncompatible { + for _, supportedVersion := range supportedVersions { + if configVersion == supportedVersion { return nil } } return &ErrorIncompatible{ - Config: configVersion, - Plugin: pluginVersions, + Config: configVersion, + Supported: supportedVersions, } } diff --git a/version/reconcile_test.go b/version/reconcile_test.go index 19a9e23f..0c964cea 100644 --- a/version/reconcile_test.go +++ b/version/reconcile_test.go @@ -41,8 +41,8 @@ var _ = Describe("Reconcile versions of net config with versions supported by pl err := reconciler.Check("0.1.0", pluginInfo) Expect(err).To(Equal(&version.ErrorIncompatible{ - Config: "0.1.0", - Plugin: []string{"1.2.3", "4.3.2"}, + Config: "0.1.0", + Supported: []string{"1.2.3", "4.3.2"}, })) Expect(err.Error()).To(Equal(`incompatible CNI versions: config is "0.1.0", plugin supports ["1.2.3" "4.3.2"]`)) diff --git a/version/version.go b/version/version.go index e39c3b55..e777e52c 100644 --- a/version/version.go +++ b/version/version.go @@ -14,6 +14,13 @@ package version +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" +) + // Current reports the version of the CNI spec implemented by this library func Current() string { return "0.2.0" @@ -27,3 +34,25 @@ func Current() string { // Any future CNI spec versions which meet this definition should be added to // this list. var Legacy = PluginSupports("0.1.0", "0.2.0") + +var resultFactories = []struct { + supportedVersions []string + newResult types.ResultFactoryFunc +}{ + {current.SupportedVersions, current.NewResult}, +} + +// Finds a Result object matching the requested version (if any) and asks +// that object to parse the plugin result, returning an error if parsing failed. +func NewResult(version string, resultBytes []byte) (types.Result, error) { + reconciler := &Reconciler{} + for _, resultFactory := range resultFactories { + err := reconciler.CheckRaw(version, resultFactory.supportedVersions) + if err == nil { + // Result supports this version + return resultFactory.newResult(resultBytes) + } + } + + return nil, fmt.Errorf("unsupported CNI result version %q", version) +} From e6adfe939a0d4586fe3451b8a6967e33fdedb1b0 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Mon, 28 Nov 2016 09:33:39 -0600 Subject: [PATCH 113/134] macvlan/ipvlan: use common RenameLink method --- ip/link.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ip/link.go b/ip/link.go index 43b37390..6431bb41 100644 --- a/ip/link.go +++ b/ip/link.go @@ -90,6 +90,14 @@ func RandomVethName() (string, error) { return fmt.Sprintf("veth%x", entropy), nil } +func RenameLink(curName, newName string) error { + link, err := netlink.LinkByName(curName) + if err == nil { + err = netlink.LinkSetName(link, newName) + } + return err +} + // SetupVeth sets up a virtual ethernet link. // Should be in container netns, and will switch back to hostNS to set the host // veth end up. From b89b56dc97777dba4d2cedb3966102ad84bfe128 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 19 Jan 2017 08:44:35 -0600 Subject: [PATCH 114/134] pkg/ipam: add testcases --- ipam/ipam_test.go | 196 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 ipam/ipam_test.go diff --git a/ipam/ipam_test.go b/ipam/ipam_test.go new file mode 100644 index 00000000..622e4c8a --- /dev/null +++ b/ipam/ipam_test.go @@ -0,0 +1,196 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ipam + +import ( + "net" + "syscall" + + "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + + "github.com/vishvananda/netlink" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const LINK_NAME = "eth0" + +func ipNetEqual(a, b *net.IPNet) bool { + aPrefix, aBits := a.Mask.Size() + bPrefix, bBits := b.Mask.Size() + if aPrefix != bPrefix || aBits != bBits { + return false + } + return a.IP.Equal(b.IP) +} + +var _ = Describe("IPAM Operations", func() { + var originalNS ns.NetNS + var ipv4, ipv6, routev4, routev6 *net.IPNet + var ipgw4, ipgw6, routegwv4, routegwv6 net.IP + var result *current.Result + + BeforeEach(func() { + // Create a new NetNS so we don't modify the host + var err error + originalNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // Add master + err = netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: LINK_NAME, + }, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + ipv4, err = types.ParseCIDR("1.2.3.30/24") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv4).NotTo(BeNil()) + + _, routev4, err = net.ParseCIDR("15.5.6.8/24") + Expect(err).NotTo(HaveOccurred()) + Expect(routev4).NotTo(BeNil()) + routegwv4 = net.ParseIP("1.2.3.5") + Expect(routegwv4).NotTo(BeNil()) + + ipgw4 = net.ParseIP("1.2.3.1") + Expect(ipgw4).NotTo(BeNil()) + + ipv6, err = types.ParseCIDR("abcd:1234:ffff::cdde/64") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv6).NotTo(BeNil()) + + _, routev6, err = net.ParseCIDR("1111:dddd::aaaa/80") + Expect(err).NotTo(HaveOccurred()) + Expect(routev6).NotTo(BeNil()) + routegwv6 = net.ParseIP("abcd:1234:ffff::10") + Expect(routegwv6).NotTo(BeNil()) + + ipgw6 = net.ParseIP("abcd:1234:ffff::1") + Expect(ipgw6).NotTo(BeNil()) + + result = ¤t.Result{ + IP4: ¤t.IPConfig{ + IP: *ipv4, + Gateway: ipgw4, + Routes: []types.Route{ + {Dst: *routev4, GW: routegwv4}, + }, + }, + IP6: ¤t.IPConfig{ + IP: *ipv6, + Gateway: ipgw6, + Routes: []types.Route{ + {Dst: *routev6, GW: routegwv6}, + }, + }, + } + }) + + AfterEach(func() { + Expect(originalNS.Close()).To(Succeed()) + }) + + It("configures a link with addresses and routes", func() { + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ConfigureIface(LINK_NAME, result) + Expect(err).NotTo(HaveOccurred()) + + link, err := netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(LINK_NAME)) + + v4addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(v4addrs)).To(Equal(1)) + Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true)) + + // Doesn't support IPv6 yet so only link-local address expected + v6addrs, err := netlink.AddrList(link, syscall.AF_INET6) + Expect(err).NotTo(HaveOccurred()) + Expect(len(v6addrs)).To(Equal(1)) + + // Ensure the v4 route + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var v4found bool + for _, route := range routes { + isv4 := route.Dst.IP.To4() != nil + if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) { + v4found = true + break + } + } + Expect(v4found).To(Equal(true)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures a link with routes using address gateways", func() { + result.IP4.Routes[0].GW = nil + result.IP6.Routes[0].GW = nil + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ConfigureIface(LINK_NAME, result) + Expect(err).NotTo(HaveOccurred()) + + link, err := netlink.LinkByName(LINK_NAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(LINK_NAME)) + + // Ensure the v4 route + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var v4found bool + for _, route := range routes { + isv4 := route.Dst.IP.To4() != nil + if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) { + v4found = true + break + } + } + Expect(v4found).To(Equal(true)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error when configuring the wrong interface", func() { + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface("asdfasdf", result) + }) + Expect(err).To(HaveOccurred()) + }) +}) From 06b397912bf4d3ad97119930de313339be02cecd Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 22 Nov 2016 11:32:35 -0600 Subject: [PATCH 115/134] spec/plugins: return interface details and multiple IP addresses to runtime Updates the spec and plugins to return an array of interfaces and IP details to the runtime including: - interface names and MAC addresses configured by the plugin - whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc) - multiple IP addresses configured by IPAM and which interface they have been assigned to Returning interface details is useful for runtimes, as well as allowing more flexible chaining of CNI plugins themselves. For example, some meta plugins may need to know the host-side interface to be able to apply firewall or traffic shaping rules to the container. --- invoke/exec_test.go | 7 +- invoke/raw_exec_test.go | 2 +- ipam/ipam.go | 36 ++++- ipam/ipam_test.go | 98 +++++++++++--- skel/skel_test.go | 4 +- types/020/types.go | 133 ++++++++++++++++++ types/020/types_suite_test.go | 27 ++++ types/020/types_test.go | 128 ++++++++++++++++++ types/current/types.go | 202 +++++++++++++++++++++++----- types/current/types_suite_test.go | 2 +- types/current/types_test.go | 160 +++++++++++++++++----- types/types.go | 5 + version/legacy_examples/examples.go | 6 +- version/version.go | 5 +- 14 files changed, 709 insertions(+), 106 deletions(-) create mode 100644 types/020/types.go create mode 100644 types/020/types_suite_test.go create mode 100644 types/020/types_test.go diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 3e207c14..7e804ab7 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -40,7 +40,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { BeforeEach(func() { rawExec = &fakes.RawExec{} - rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ip4": { "ip": "1.2.3.4/24" } }`) + rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`) versionDecoder = &fakes.VersionDecoder{} versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") @@ -50,7 +50,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { VersionDecoder: versionDecoder, } pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin", "cniVersion": "0.2.0" }`) + netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`) cniargs = &fakes.CNIArgs{} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) @@ -62,7 +62,8 @@ var _ = Describe("Executing a plugin, unit tests", func() { result, err := current.GetResult(r) Expect(err).NotTo(HaveOccurred()) - Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4")) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4")) }) It("passes its arguments through to the rawExec", func() { diff --git a/invoke/raw_exec_test.go b/invoke/raw_exec_test.go index b0ca9607..5ab23ae5 100644 --- a/invoke/raw_exec_test.go +++ b/invoke/raw_exec_test.go @@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() { "CNI_PATH=/some/bin/path", "CNI_IFNAME=some-eth0", } - stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.2.0"}`) + stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`) execer = &invoke.RawExec{} }) diff --git a/ipam/ipam.go b/ipam/ipam.go index 8dd861a6..b76780f0 100644 --- a/ipam/ipam.go +++ b/ipam/ipam.go @@ -16,6 +16,7 @@ package ipam import ( "fmt" + "net" "os" "github.com/containernetworking/cni/pkg/invoke" @@ -37,6 +38,10 @@ func ExecDel(plugin string, netconf []byte) error { // ConfigureIface takes the result of IPAM plugin and // applies to the ifName interface func ConfigureIface(ifName string, res *current.Result) error { + if len(res.Interfaces) == 0 { + return fmt.Errorf("no interfaces to configure") + } + link, err := netlink.LinkByName(ifName) if err != nil { return fmt.Errorf("failed to lookup %q: %v", ifName, err) @@ -46,16 +51,35 @@ func ConfigureIface(ifName string, res *current.Result) error { return fmt.Errorf("failed to set %q UP: %v", ifName, err) } - // TODO(eyakubovich): IPv6 - addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} - if err = netlink.AddrAdd(link, addr); err != nil { - return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) + var v4gw, v6gw net.IP + for _, ipc := range res.IPs { + if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName { + // IP address is for a different interface + return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName) + } + + addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err) + } + + gwIsV4 := ipc.Gateway.To4() != nil + if gwIsV4 && v4gw == nil { + v4gw = ipc.Gateway + } else if !gwIsV4 && v6gw == nil { + v6gw = ipc.Gateway + } } - for _, r := range res.IP4.Routes { + for _, r := range res.Routes { + routeIsV4 := r.Dst.IP.To4() != nil gw := r.GW if gw == nil { - gw = res.IP4.Gateway + if routeIsV4 && v4gw != nil { + gw = v4gw + } else if !routeIsV4 && v6gw != nil { + gw = v6gw + } } if err = ip.AddRoute(&r.Dst, gw, link); err != nil { // we skip over duplicate routes as we assume the first one wins diff --git a/ipam/ipam_test.go b/ipam/ipam_test.go index 622e4c8a..2d27825d 100644 --- a/ipam/ipam_test.go +++ b/ipam/ipam_test.go @@ -94,19 +94,35 @@ var _ = Describe("IPAM Operations", func() { Expect(ipgw6).NotTo(BeNil()) result = ¤t.Result{ - IP4: ¤t.IPConfig{ - IP: *ipv4, - Gateway: ipgw4, - Routes: []types.Route{ - {Dst: *routev4, GW: routegwv4}, + Interfaces: []*current.Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:55", + Sandbox: "/proc/3553/ns/net", + }, + { + Name: "fake0", + Mac: "00:33:44:55:66:77", + Sandbox: "/proc/1234/ns/net", }, }, - IP6: ¤t.IPConfig{ - IP: *ipv6, - Gateway: ipgw6, - Routes: []types.Route{ - {Dst: *routev6, GW: routegwv6}, + IPs: []*current.IPConfig{ + { + Version: "4", + Interface: 0, + Address: *ipv4, + Gateway: ipgw4, }, + { + Version: "6", + Interface: 0, + Address: *ipv6, + Gateway: ipgw6, + }, + }, + Routes: []*types.Route{ + {Dst: *routev4, GW: routegwv4}, + {Dst: *routev6, GW: routegwv6}, }, } }) @@ -131,24 +147,39 @@ var _ = Describe("IPAM Operations", func() { Expect(len(v4addrs)).To(Equal(1)) Expect(ipNetEqual(v4addrs[0].IPNet, ipv4)).To(Equal(true)) - // Doesn't support IPv6 yet so only link-local address expected v6addrs, err := netlink.AddrList(link, syscall.AF_INET6) Expect(err).NotTo(HaveOccurred()) - Expect(len(v6addrs)).To(Equal(1)) + Expect(len(v6addrs)).To(Equal(2)) - // Ensure the v4 route + var found bool + for _, a := range v6addrs { + if ipNetEqual(a.IPNet, ipv6) { + found = true + break + } + } + Expect(found).To(Equal(true)) + + // Ensure the v4 route, v6 route, and subnet route routes, err := netlink.RouteList(link, 0) Expect(err).NotTo(HaveOccurred()) - var v4found bool + var v4found, v6found bool for _, route := range routes { isv4 := route.Dst.IP.To4() != nil if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(routegwv4) { v4found = true + } + if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(routegwv6) { + v6found = true + } + + if v4found && v6found { break } } Expect(v4found).To(Equal(true)) + Expect(v6found).To(Equal(true)) return nil }) @@ -156,8 +187,8 @@ var _ = Describe("IPAM Operations", func() { }) It("configures a link with routes using address gateways", func() { - result.IP4.Routes[0].GW = nil - result.IP6.Routes[0].GW = nil + result.Routes[0].GW = nil + result.Routes[1].GW = nil err := originalNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -168,25 +199,56 @@ var _ = Describe("IPAM Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(LINK_NAME)) - // Ensure the v4 route + // Ensure the v4 route, v6 route, and subnet route routes, err := netlink.RouteList(link, 0) Expect(err).NotTo(HaveOccurred()) - var v4found bool + var v4found, v6found bool for _, route := range routes { isv4 := route.Dst.IP.To4() != nil if isv4 && ipNetEqual(route.Dst, routev4) && route.Gw.Equal(ipgw4) { v4found = true + } + if !isv4 && ipNetEqual(route.Dst, routev6) && route.Gw.Equal(ipgw6) { + v6found = true + } + + if v4found && v6found { break } } Expect(v4found).To(Equal(true)) + Expect(v6found).To(Equal(true)) return nil }) Expect(err).NotTo(HaveOccurred()) }) + It("returns an error when the interface index doesn't match the link name", func() { + result.IPs[0].Interface = 1 + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when the interface index is too big", func() { + result.IPs[0].Interface = 2 + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error when there are no interfaces to configure", func() { + result.Interfaces = []*current.Interface{} + err := originalNS.Do(func(ns.NetNS) error { + return ConfigureIface(LINK_NAME, result) + }) + Expect(err).To(HaveOccurred()) + }) + It("returns an error when configuring the wrong interface", func() { err := originalNS.Do(func(ns.NetNS) error { return ConfigureIface("asdfasdf", result) diff --git a/skel/skel_test.go b/skel/skel_test.go index 6652fcdb..d7f729f0 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -226,7 +226,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "supportedVersions": ["9.8.7"] }`)) }) @@ -258,7 +258,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.2.0", + "cniVersion": "0.3.0", "supportedVersions": ["9.8.7"] }`)) }) diff --git a/types/020/types.go b/types/020/types.go new file mode 100644 index 00000000..666cfe93 --- /dev/null +++ b/types/020/types.go @@ -0,0 +1,133 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types020 + +import ( + "encoding/json" + "fmt" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" +) + +const implementedSpecVersion string = "0.2.0" + +var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} + +// Compatibility types for CNI version 0.1.0 and 0.2.0 + +func NewResult(data []byte) (types.Result, error) { + result := &Result{} + if err := json.Unmarshal(data, result); err != nil { + return nil, err + } + return result, nil +} + +func GetResult(r types.Result) (*Result, error) { + // We expect version 0.1.0/0.2.0 results + result020, err := r.GetAsVersion(implementedSpecVersion) + if err != nil { + return nil, err + } + result, ok := result020.(*Result) + if !ok { + return nil, fmt.Errorf("failed to convert result") + } + return result, nil +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` + DNS types.DNS `json:"dns,omitempty"` +} + +func (r *Result) Version() string { + return implementedSpecVersion +} + +func (r *Result) GetAsVersion(version string) (types.Result, error) { + for _, supportedVersion := range SupportedVersions { + if version == supportedVersion { + return r, nil + } + } + return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) +} + +func (r *Result) Print() error { + data, err := json.MarshalIndent(r, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(data) + return err +} + +// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where +// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the +// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. +func (r *Result) String() string { + var str string + if r.IP4 != nil { + str = fmt.Sprintf("IP4:%+v, ", *r.IP4) + } + if r.IP6 != nil { + str += fmt.Sprintf("IP6:%+v, ", *r.IP6) + } + return fmt.Sprintf("%sDNS:%+v", str, r.DNS) +} + +// IPConfig contains values necessary to configure an interface +type IPConfig struct { + IP net.IPNet + Gateway net.IP + Routes []types.Route +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP types.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []types.Route `json:"routes,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: types.IPNet(c.IP), + Gateway: c.Gateway, + Routes: c.Routes, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.IP = net.IPNet(ipc.IP) + c.Gateway = ipc.Gateway + c.Routes = ipc.Routes + return nil +} diff --git a/types/020/types_suite_test.go b/types/020/types_suite_test.go new file mode 100644 index 00000000..095d73e2 --- /dev/null +++ b/types/020/types_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types020_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestTypes010(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "0.1.0/0.2.0 Types Suite") +} diff --git a/types/020/types_test.go b/types/020/types_test.go new file mode 100644 index 00000000..1bcdda73 --- /dev/null +++ b/types/020/types_test.go @@ -0,0 +1,128 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types020_test + +import ( + "io/ioutil" + "net" + "os" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() { + It("correctly encodes a 0.1.0/0.2.0 Result", func() { + ipv4, err := types.ParseCIDR("1.2.3.30/24") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv4).NotTo(BeNil()) + + routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") + Expect(err).NotTo(HaveOccurred()) + Expect(routev4).NotTo(BeNil()) + Expect(routegwv4).NotTo(BeNil()) + + ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv6).NotTo(BeNil()) + + routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") + Expect(err).NotTo(HaveOccurred()) + Expect(routev6).NotTo(BeNil()) + Expect(routegwv6).NotTo(BeNil()) + + // Set every field of the struct to ensure source compatibility + res := types020.Result{ + IP4: &types020.IPConfig{ + IP: *ipv4, + Gateway: net.ParseIP("1.2.3.1"), + Routes: []types.Route{ + {Dst: *routev4, GW: routegwv4}, + }, + }, + IP6: &types020.IPConfig{ + IP: *ipv6, + Gateway: net.ParseIP("abcd:1234:ffff::1"), + Routes: []types.Route{ + {Dst: *routev6, GW: routegwv6}, + }, + }, + DNS: types.DNS{ + Nameservers: []string{"1.2.3.4", "1::cafe"}, + Domain: "acompany.com", + Search: []string{"somedomain.com", "otherdomain.net"}, + Options: []string{"foo", "bar"}, + }, + } + + Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) + + // Redirect stdout to capture JSON result + oldStdout := os.Stdout + r, w, err := os.Pipe() + Expect(err).NotTo(HaveOccurred()) + + os.Stdout = w + err = res.Print() + w.Close() + Expect(err).NotTo(HaveOccurred()) + + // parse the result + out, err := ioutil.ReadAll(r) + os.Stdout = oldStdout + Expect(err).NotTo(HaveOccurred()) + + Expect(string(out)).To(Equal(`{ + "ip4": { + "ip": "1.2.3.30/24", + "gateway": "1.2.3.1", + "routes": [ + { + "dst": "15.5.6.0/24", + "gw": "15.5.6.8" + } + ] + }, + "ip6": { + "ip": "abcd:1234:ffff::cdde/64", + "gateway": "abcd:1234:ffff::1", + "routes": [ + { + "dst": "1111:dddd::/80", + "gw": "1111:dddd::aaaa" + } + ] + }, + "dns": { + "nameservers": [ + "1.2.3.4", + "1::cafe" + ], + "domain": "acompany.com", + "search": [ + "somedomain.com", + "otherdomain.net" + ], + "options": [ + "foo", + "bar" + ] + } +}`)) + }) +}) diff --git a/types/current/types.go b/types/current/types.go index 338b3fd2..e686a9a7 100644 --- a/types/current/types.go +++ b/types/current/types.go @@ -21,11 +21,12 @@ import ( "os" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" ) -const implementedSpecVersion string = "0.2.0" +const implementedSpecVersion string = "0.3.0" -var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} +var SupportedVersions = []string{implementedSpecVersion} func NewResult(data []byte) (types.Result, error) { result := &Result{} @@ -36,11 +37,11 @@ func NewResult(data []byte) (types.Result, error) { } func GetResult(r types.Result) (*Result, error) { - newResult, err := r.GetAsVersion(implementedSpecVersion) + resultCurrent, err := r.GetAsVersion(implementedSpecVersion) if err != nil { return nil, err } - result, ok := newResult.(*Result) + result, ok := resultCurrent.(*Result) if !ok { return nil, fmt.Errorf("failed to convert result") } @@ -51,10 +52,67 @@ var resultConverters = []struct { versions []string convert func(types.Result) (*Result, error) }{ - {SupportedVersions, convertFrom020}, + {types020.SupportedVersions, convertFrom020}, + {SupportedVersions, convertFrom030}, } func convertFrom020(result types.Result) (*Result, error) { + oldResult, err := types020.GetResult(result) + if err != nil { + return nil, err + } + + newResult := &Result{ + DNS: oldResult.DNS, + Routes: []*types.Route{}, + } + + if oldResult.IP4 != nil { + newResult.IPs = append(newResult.IPs, &IPConfig{ + Version: "4", + Interface: -1, + Address: oldResult.IP4.IP, + Gateway: oldResult.IP4.Gateway, + }) + for _, route := range oldResult.IP4.Routes { + gw := route.GW + if gw == nil { + gw = oldResult.IP4.Gateway + } + newResult.Routes = append(newResult.Routes, &types.Route{ + Dst: route.Dst, + GW: gw, + }) + } + } + + if oldResult.IP6 != nil { + newResult.IPs = append(newResult.IPs, &IPConfig{ + Version: "6", + Interface: -1, + Address: oldResult.IP6.IP, + Gateway: oldResult.IP6.Gateway, + }) + for _, route := range oldResult.IP6.Routes { + gw := route.GW + if gw == nil { + gw = oldResult.IP6.Gateway + } + newResult.Routes = append(newResult.Routes, &types.Route{ + Dst: route.Dst, + GW: gw, + }) + } + } + + if len(newResult.IPs) == 0 { + return nil, fmt.Errorf("cannot convert: no valid IP addresses") + } + + return newResult, nil +} + +func convertFrom030(result types.Result) (*Result, error) { newResult, ok := result.(*Result) if !ok { return nil, fmt.Errorf("failed to convert result") @@ -76,9 +134,58 @@ func NewResultFromResult(result types.Result) (*Result, error) { // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { - IP4 *IPConfig `json:"ip4,omitempty"` - IP6 *IPConfig `json:"ip6,omitempty"` - DNS types.DNS `json:"dns,omitempty"` + Interfaces []*Interface `json:"interfaces,omitempty"` + IPs []*IPConfig `json:"ips,omitempty"` + Routes []*types.Route `json:"routes,omitempty"` + DNS types.DNS `json:"dns,omitempty"` +} + +// Convert to the older 0.2.0 CNI spec Result type +func (r *Result) convertTo020() (*types020.Result, error) { + oldResult := &types020.Result{ + DNS: r.DNS, + } + + for _, ip := range r.IPs { + // Only convert the first IP address of each version as 0.2.0 + // and earlier cannot handle multiple IP addresses + if ip.Version == "4" && oldResult.IP4 == nil { + oldResult.IP4 = &types020.IPConfig{ + IP: ip.Address, + Gateway: ip.Gateway, + } + } else if ip.Version == "6" && oldResult.IP6 == nil { + oldResult.IP6 = &types020.IPConfig{ + IP: ip.Address, + Gateway: ip.Gateway, + } + } + + if oldResult.IP4 != nil && oldResult.IP6 != nil { + break + } + } + + for _, route := range r.Routes { + is4 := route.Dst.IP.To4() != nil + if is4 && oldResult.IP4 != nil { + oldResult.IP4.Routes = append(oldResult.IP4.Routes, types.Route{ + Dst: route.Dst, + GW: route.GW, + }) + } else if !is4 && oldResult.IP6 != nil { + oldResult.IP6.Routes = append(oldResult.IP6.Routes, types.Route{ + Dst: route.Dst, + GW: route.GW, + }) + } + } + + if oldResult.IP4 == nil && oldResult.IP6 == nil { + return nil, fmt.Errorf("cannot convert: no valid IP addresses") + } + + return oldResult, nil } func (r *Result) Version() string { @@ -86,12 +193,13 @@ func (r *Result) Version() string { } func (r *Result) GetAsVersion(version string) (types.Result, error) { - for _, supportedVersion := range SupportedVersions { - if version == supportedVersion { - return r, nil - } + switch version { + case implementedSpecVersion: + return r, nil + case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: + return r.convertTo020() } - return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) + return nil, fmt.Errorf("cannot convert version 0.3.0 to %q", version) } func (r *Result) Print() error { @@ -103,42 +211,67 @@ func (r *Result) Print() error { return err } -// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where -// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the +// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where +// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the // receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. func (r *Result) String() string { var str string - if r.IP4 != nil { - str = fmt.Sprintf("IP4:%+v, ", *r.IP4) + if len(r.Interfaces) > 0 { + str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces) } - if r.IP6 != nil { - str += fmt.Sprintf("IP6:%+v, ", *r.IP6) + if len(r.IPs) > 0 { + str += fmt.Sprintf("IP:%+v, ", r.IPs) + } + if len(r.Routes) > 0 { + str += fmt.Sprintf("Routes:%+v, ", r.Routes) } return fmt.Sprintf("%sDNS:%+v", str, r.DNS) } -// IPConfig contains values necessary to configure an interface -type IPConfig struct { - IP net.IPNet - Gateway net.IP - Routes []types.Route +// Convert this old version result to the current CNI version result +func (r *Result) Convert() (*Result, error) { + return r, nil } -// net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom IPNet type +// Interface contains values about the created interfaces +type Interface struct { + Name string `json:"name"` + Mac string `json:"mac,omitempty"` + Sandbox string `json:"sandbox,omitempty"` +} + +func (i *Interface) String() string { + return fmt.Sprintf("%+v", *i) +} + +// IPConfig contains values necessary to configure an IP address on an interface +type IPConfig struct { + // IP version, either "4" or "6" + Version string + // Index into Result structs Interfaces list + Interface int + Address net.IPNet + Gateway net.IP +} + +func (i *IPConfig) String() string { + return fmt.Sprintf("%+v", *i) +} // JSON (un)marshallable types type ipConfig struct { - IP types.IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []types.Route `json:"routes,omitempty"` + Version string `json:"version"` + Interface int `json:"interface,omitempty"` + Address types.IPNet `json:"address"` + Gateway net.IP `json:"gateway,omitempty"` } func (c *IPConfig) MarshalJSON() ([]byte, error) { ipc := ipConfig{ - IP: types.IPNet(c.IP), - Gateway: c.Gateway, - Routes: c.Routes, + Version: c.Version, + Interface: c.Interface, + Address: types.IPNet(c.Address), + Gateway: c.Gateway, } return json.Marshal(ipc) @@ -150,8 +283,9 @@ func (c *IPConfig) UnmarshalJSON(data []byte) error { return err } - c.IP = net.IPNet(ipc.IP) + c.Version = ipc.Version + c.Interface = ipc.Interface + c.Address = net.IPNet(ipc.Address) c.Gateway = ipc.Gateway - c.Routes = ipc.Routes return nil } diff --git a/types/current/types_suite_test.go b/types/current/types_suite_test.go index 42a47a25..89cccecd 100644 --- a/types/current/types_suite_test.go +++ b/types/current/types_suite_test.go @@ -23,5 +23,5 @@ import ( func TestTypes010(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "0.1.0 Types Suite") + RunSpecs(t, "0.3.0 Types Suite") } diff --git a/types/current/types_test.go b/types/current/types_test.go index 3810999d..f839cc90 100644 --- a/types/current/types_test.go +++ b/types/current/types_test.go @@ -26,51 +26,66 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { - It("correctly encodes a 0.1.0 Result", func() { - ipv4, err := types.ParseCIDR("1.2.3.30/24") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv4).NotTo(BeNil()) +func testResult() *current.Result { + ipv4, err := types.ParseCIDR("1.2.3.30/24") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv4).NotTo(BeNil()) - routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") - Expect(err).NotTo(HaveOccurred()) - Expect(routev4).NotTo(BeNil()) - Expect(routegwv4).NotTo(BeNil()) + routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") + Expect(err).NotTo(HaveOccurred()) + Expect(routev4).NotTo(BeNil()) + Expect(routegwv4).NotTo(BeNil()) - ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv6).NotTo(BeNil()) + ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") + Expect(err).NotTo(HaveOccurred()) + Expect(ipv6).NotTo(BeNil()) - routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") - Expect(err).NotTo(HaveOccurred()) - Expect(routev6).NotTo(BeNil()) - Expect(routegwv6).NotTo(BeNil()) + routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") + Expect(err).NotTo(HaveOccurred()) + Expect(routev6).NotTo(BeNil()) + Expect(routegwv6).NotTo(BeNil()) - // Set every field of the struct to ensure source compatibility - res := current.Result{ - IP4: ¤t.IPConfig{ - IP: *ipv4, - Gateway: net.ParseIP("1.2.3.1"), - Routes: []types.Route{ - {Dst: *routev4, GW: routegwv4}, - }, + // Set every field of the struct to ensure source compatibility + return ¤t.Result{ + Interfaces: []*current.Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:55", + Sandbox: "/proc/3553/ns/net", }, - IP6: ¤t.IPConfig{ - IP: *ipv6, - Gateway: net.ParseIP("abcd:1234:ffff::1"), - Routes: []types.Route{ - {Dst: *routev6, GW: routegwv6}, - }, + }, + IPs: []*current.IPConfig{ + { + Version: "4", + Interface: 0, + Address: *ipv4, + Gateway: net.ParseIP("1.2.3.1"), }, - DNS: types.DNS{ - Nameservers: []string{"1.2.3.4", "1::cafe"}, - Domain: "acompany.com", - Search: []string{"somedomain.com", "otherdomain.net"}, - Options: []string{"foo", "bar"}, + { + Version: "6", + Interface: 0, + Address: *ipv6, + Gateway: net.ParseIP("abcd:1234:ffff::1"), }, - } + }, + Routes: []*types.Route{ + {Dst: *routev4, GW: routegwv4}, + {Dst: *routev6, GW: routegwv6}, + }, + DNS: types.DNS{ + Nameservers: []string{"1.2.3.4", "1::cafe"}, + Domain: "acompany.com", + Search: []string{"somedomain.com", "otherdomain.net"}, + Options: []string{"foo", "bar"}, + }, + } +} - Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) +var _ = Describe("Ensures compatibility with the 0.3.0 spec", func() { + It("correctly encodes a 0.3.0 Result", func() { + res := testResult() + + Expect(res.String()).To(Equal("Interfaces:[{Name:eth0 Mac:00:11:22:33:44:55 Sandbox:/proc/3553/ns/net}], IP:[{Version:4 Interface:0 Address:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1} {Version:6 Interface:0 Address:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1}], Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8} {Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}], DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) // Redirect stdout to capture JSON result oldStdout := os.Stdout @@ -88,6 +103,76 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ + "interfaces": [ + { + "name": "eth0", + "mac": "00:11:22:33:44:55", + "sandbox": "/proc/3553/ns/net" + } + ], + "ips": [ + { + "version": "4", + "address": "1.2.3.30/24", + "gateway": "1.2.3.1" + }, + { + "version": "6", + "address": "abcd:1234:ffff::cdde/64", + "gateway": "abcd:1234:ffff::1" + } + ], + "routes": [ + { + "dst": "15.5.6.0/24", + "gw": "15.5.6.8" + }, + { + "dst": "1111:dddd::/80", + "gw": "1111:dddd::aaaa" + } + ], + "dns": { + "nameservers": [ + "1.2.3.4", + "1::cafe" + ], + "domain": "acompany.com", + "search": [ + "somedomain.com", + "otherdomain.net" + ], + "options": [ + "foo", + "bar" + ] + } +}`)) + }) + + var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { + It("correctly encodes a 0.1.0 Result", func() { + res, err := testResult().GetAsVersion("0.1.0") + Expect(err).NotTo(HaveOccurred()) + + Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) + + // Redirect stdout to capture JSON result + oldStdout := os.Stdout + r, w, err := os.Pipe() + Expect(err).NotTo(HaveOccurred()) + + os.Stdout = w + err = res.Print() + w.Close() + Expect(err).NotTo(HaveOccurred()) + + // parse the result + out, err := ioutil.ReadAll(r) + os.Stdout = oldStdout + Expect(err).NotTo(HaveOccurred()) + + Expect(string(out)).To(Equal(`{ "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", @@ -124,5 +209,6 @@ var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { ] } }`)) + }) }) }) diff --git a/types/types.go b/types/types.go index 2ceffebc..a81ac702 100644 --- a/types/types.go +++ b/types/types.go @@ -16,6 +16,7 @@ package types import ( "encoding/json" + "fmt" "net" "os" ) @@ -114,6 +115,10 @@ type Route struct { GW net.IP } +func (r *Route) String() string { + return fmt.Sprintf("%+v", *r) +} + // Well known error codes // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes const ( diff --git a/version/legacy_examples/examples.go b/version/legacy_examples/examples.go index 8b079a3d..1bf406b3 100644 --- a/version/legacy_examples/examples.go +++ b/version/legacy_examples/examples.go @@ -23,7 +23,7 @@ import ( "sync" "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/version/testhelpers" ) @@ -115,8 +115,8 @@ func main() { skel.PluginMain(c, c) } // // As we change the CNI spec, the Result type and this value may change. // The text of the example plugins should not. -var ExpectedResult = ¤t.Result{ - IP4: ¤t.IPConfig{ +var ExpectedResult = &types020.Result{ + IP4: &types020.IPConfig{ IP: net.IPNet{ IP: net.ParseIP("10.1.2.3"), Mask: net.CIDRMask(24, 32), diff --git a/version/version.go b/version/version.go index e777e52c..7c589633 100644 --- a/version/version.go +++ b/version/version.go @@ -18,12 +18,13 @@ import ( "fmt" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" ) // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.2.0" + return "0.3.0" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -34,12 +35,14 @@ func Current() string { // Any future CNI spec versions which meet this definition should be added to // this list. var Legacy = PluginSupports("0.1.0", "0.2.0") +var All = PluginSupports("0.1.0", "0.2.0", "0.3.0") var resultFactories = []struct { supportedVersions []string newResult types.ResultFactoryFunc }{ {current.SupportedVersions, current.NewResult}, + {types020.SupportedVersions, types020.NewResult}, } // Finds a Result object matching the requested version (if any) and asks From 46e17b26ffcbe53b017f2acf9d13e685b0fd745b Mon Sep 17 00:00:00 2001 From: Onur Date: Sat, 28 Jan 2017 13:30:00 -0800 Subject: [PATCH 116/134] spec, libcni, pkg/invoke: Use OS-agnostic separator when parsing CNI_PATH Hardcoding the list separator character as ":" causes CNI to fail when parsing CNI_PATH on other operating systems. For example, Windows uses ";" as list separator because ":" can legally appear in paths such as "C:\path\to\file". This change replaces use of ":" with OS-agnostic APIs or os.PathListSeparator. Fixes #358 --- invoke/delegate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invoke/delegate.go b/invoke/delegate.go index f25beddc..c78a69ee 100644 --- a/invoke/delegate.go +++ b/invoke/delegate.go @@ -17,7 +17,7 @@ package invoke import ( "fmt" "os" - "strings" + "path/filepath" "github.com/containernetworking/cni/pkg/types" ) @@ -27,7 +27,7 @@ func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) { return nil, fmt.Errorf("CNI_COMMAND is not ADD") } - paths := strings.Split(os.Getenv("CNI_PATH"), ":") + paths := filepath.SplitList(os.Getenv("CNI_PATH")) pluginPath, err := FindInPath(delegatePlugin, paths) if err != nil { @@ -42,7 +42,7 @@ func DelegateDel(delegatePlugin string, netconf []byte) error { return fmt.Errorf("CNI_COMMAND is not DEL") } - paths := strings.Split(os.Getenv("CNI_PATH"), ":") + paths := filepath.SplitList(os.Getenv("CNI_PATH")) pluginPath, err := FindInPath(delegatePlugin, paths) if err != nil { From 162b73e44f17cf9a9f4481253c331070733d043f Mon Sep 17 00:00:00 2001 From: Paulo Pires Date: Fri, 3 Feb 2017 04:46:55 +0000 Subject: [PATCH 117/134] pkg/utils/sysctl/sysctl_linux.go: fix build tag. --- utils/sysctl/sysctl_linux.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/sysctl/sysctl_linux.go b/utils/sysctl/sysctl_linux.go index c0fba382..118bd279 100644 --- a/utils/sysctl/sysctl_linux.go +++ b/utils/sysctl/sysctl_linux.go @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// build +linux - package sysctl import ( From 2e90a8cc33b580fc0ef9511bb6144412b98bec57 Mon Sep 17 00:00:00 2001 From: Paulo Pires Date: Fri, 3 Feb 2017 04:47:43 +0000 Subject: [PATCH 118/134] pkg/utils/sysctl/sysctl_linux.go: fix typo. --- utils/sysctl/sysctl_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/sysctl/sysctl_linux.go b/utils/sysctl/sysctl_linux.go index 118bd279..fe06d2d9 100644 --- a/utils/sysctl/sysctl_linux.go +++ b/utils/sysctl/sysctl_linux.go @@ -24,7 +24,7 @@ import ( // Sysctl provides a method to set/get values from /proc/sys - in linux systems // new interface to set/get values of variables formerly handled by sysctl syscall // If optional `params` have only one string value - this function will -// set this value into coresponding sysctl variable +// set this value into corresponding sysctl variable func Sysctl(name string, params ...string) (string, error) { if len(params) > 1 { return "", fmt.Errorf("unexcepted additional parameters") From a35f8ae914418a48c433618a592b849eb3e61c6a Mon Sep 17 00:00:00 2001 From: Onur Filiz Date: Sat, 4 Feb 2017 12:01:47 -0800 Subject: [PATCH 119/134] invoke: Enable plugin file names with extensions A CNI network configuration file contains the plugin's executable file name. Some platforms like Windows require a file name extension for executables. This causes unnecessary burden on admins as they now have to maintain two versions of each type of netconfig file, which differ only by the ".exe" extension. A much simpler design is for invoke package to also look for well-known extensions on platforms that require it. Existing tests are improved and new tests are added to cover the new behavior. Fixes #360 --- invoke/find.go | 16 ++++++---------- invoke/find_test.go | 33 +++++++++++++++++++++++++++++---- invoke/os_unix.go | 20 ++++++++++++++++++++ invoke/os_windows.go | 18 ++++++++++++++++++ 4 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 invoke/os_unix.go create mode 100644 invoke/os_windows.go diff --git a/invoke/find.go b/invoke/find.go index 3b037907..e815404c 100644 --- a/invoke/find.go +++ b/invoke/find.go @@ -30,18 +30,14 @@ func FindInPath(plugin string, paths []string) (string, error) { return "", fmt.Errorf("no paths provided") } - var fullpath string for _, path := range paths { - full := filepath.Join(path, plugin) - if fi, err := os.Stat(full); err == nil && fi.Mode().IsRegular() { - fullpath = full - break + for _, fe := range ExecutableFileExtensions { + fullpath := filepath.Join(path, plugin) + fe + if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() { + return fullpath, nil + } } } - if fullpath == "" { - return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths) - } - - return fullpath, nil + return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths) } diff --git a/invoke/find_test.go b/invoke/find_test.go index be4cc2dd..58543131 100644 --- a/invoke/find_test.go +++ b/invoke/find_test.go @@ -17,7 +17,9 @@ package invoke_test import ( "fmt" "io/ioutil" + "os" "path/filepath" + "strings" "github.com/containernetworking/cni/pkg/invoke" . "github.com/onsi/ginkgo" @@ -26,10 +28,12 @@ import ( var _ = Describe("FindInPath", func() { var ( - multiplePaths []string - pluginName string - pluginDir string - anotherTempDir string + multiplePaths []string + pluginName string + plugin2NameWithExt string + plugin2NameWithoutExt string + pluginDir string + anotherTempDir string ) BeforeEach(func() { @@ -37,11 +41,24 @@ var _ = Describe("FindInPath", func() { Expect(err).NotTo(HaveOccurred()) plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin") + Expect(err).NotTo(HaveOccurred()) + + plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0] + plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name)) + Expect(err).NotTo(HaveOccurred()) anotherTempDir, err = ioutil.TempDir("", "nothing-here") + Expect(err).NotTo(HaveOccurred()) multiplePaths = []string{anotherTempDir, tempDir} pluginDir, pluginName = filepath.Split(plugin.Name()) + _, plugin2NameWithExt = filepath.Split(plugin2.Name()) + plugin2NameWithoutExt = strings.Split(plugin2NameWithExt, ".")[0] + }) + + AfterEach(func() { + os.RemoveAll(pluginDir) + os.RemoveAll(anotherTempDir) }) Context("when multiple paths are provided", func() { @@ -52,6 +69,14 @@ var _ = Describe("FindInPath", func() { }) }) + Context("when a plugin name without its file name extension is provided", func() { + It("returns the path to the plugin, including its extension", func() { + pluginPath, err := invoke.FindInPath(plugin2NameWithoutExt, multiplePaths) + Expect(err).NotTo(HaveOccurred()) + Expect(pluginPath).To(Equal(filepath.Join(pluginDir, plugin2NameWithExt))) + }) + }) + Context("when an error occurs", func() { Context("when no paths are provided", func() { It("returns an error noting no paths were provided", func() { diff --git a/invoke/os_unix.go b/invoke/os_unix.go new file mode 100644 index 00000000..bab5737a --- /dev/null +++ b/invoke/os_unix.go @@ -0,0 +1,20 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin dragonfly freebsd linux netbsd opensbd solaris + +package invoke + +// Valid file extensions for plugin executables. +var ExecutableFileExtensions = []string{""} diff --git a/invoke/os_windows.go b/invoke/os_windows.go new file mode 100644 index 00000000..7665125b --- /dev/null +++ b/invoke/os_windows.go @@ -0,0 +1,18 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package invoke + +// Valid file extensions for plugin executables. +var ExecutableFileExtensions = []string{".exe", ""} From dff808c98e37c1bbe80a2615bc80db603e5e9e2c Mon Sep 17 00:00:00 2001 From: Aithal Date: Fri, 17 Feb 2017 17:04:53 -0800 Subject: [PATCH 120/134] pkg/ns: refactored so that builds succeed on non-linux platforms moved functions that depend on linux packages (sys/unix) into a separate file and added nop methods with a build tag for non-linux platforms in a new file. --- ns/ns.go | 124 +------------------------------------ ns/ns_linux.go | 144 +++++++++++++++++++++++++++++++++++++++++++ ns/ns_unspecified.go | 33 ++++++++++ 3 files changed, 180 insertions(+), 121 deletions(-) create mode 100644 ns/ns_linux.go create mode 100644 ns/ns_unspecified.go diff --git a/ns/ns.go b/ns/ns.go index 220dd694..428fb77f 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -15,15 +15,12 @@ package ns import ( - "crypto/rand" + "errors" "fmt" "os" - "path" "runtime" "sync" "syscall" - - "golang.org/x/sys/unix" ) type NetNS interface { @@ -65,12 +62,8 @@ type netNS struct { // netNS implements the NetNS interface var _ NetNS = &netNS{} -func getCurrentThreadNetNSPath() string { - // /proc/self/ns/net returns the namespace of the main thread, not - // of whatever thread this goroutine is running on. Make sure we - // use the thread's net namespace since the thread is switching around - return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) -} +// NotImplementedError is used to indicate that a method is not implemented for the given platform +var NotImplementedError = errors.New("Not Implemented") // Returns an object representing the current OS thread's network namespace func GetCurrentNS() (NetNS, error) { @@ -125,82 +118,6 @@ func GetNS(nspath string) (NetNS, error) { return &netNS{file: fd}, nil } -// Creates a new persistent network namespace and returns an object -// representing that namespace, without switching to it -func NewNS() (NetNS, error) { - const nsRunDir = "/var/run/netns" - - b := make([]byte, 16) - _, err := rand.Reader.Read(b) - if err != nil { - return nil, fmt.Errorf("failed to generate random netns name: %v", err) - } - - err = os.MkdirAll(nsRunDir, 0755) - if err != nil { - return nil, err - } - - // create an empty file at the mount point - nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) - nsPath := path.Join(nsRunDir, nsName) - mountPointFd, err := os.Create(nsPath) - if err != nil { - return nil, err - } - mountPointFd.Close() - - // Ensure the mount point is cleaned up on errors; if the namespace - // was successfully mounted this will have no effect because the file - // is in-use - defer os.RemoveAll(nsPath) - - var wg sync.WaitGroup - wg.Add(1) - - // do namespace work in a dedicated goroutine, so that we can safely - // Lock/Unlock OSThread without upsetting the lock/unlock state of - // the caller of this function - var fd *os.File - go (func() { - defer wg.Done() - runtime.LockOSThread() - - var origNS NetNS - origNS, err = GetNS(getCurrentThreadNetNSPath()) - if err != nil { - return - } - defer origNS.Close() - - // create a new netns on the current thread - err = unix.Unshare(unix.CLONE_NEWNET) - if err != nil { - return - } - defer origNS.Set() - - // bind mount the new netns from the current thread onto the mount point - err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") - if err != nil { - return - } - - fd, err = os.Open(nsPath) - if err != nil { - return - } - })() - wg.Wait() - - if err != nil { - unix.Unmount(nsPath, unix.MNT_DETACH) - return nil, fmt.Errorf("failed to create namespace: %v", err) - } - - return &netNS{file: fd, mounted: true}, nil -} - func (ns *netNS) Path() string { return ns.file.Name() } @@ -216,29 +133,6 @@ func (ns *netNS) errorIfClosed() error { return nil } -func (ns *netNS) Close() error { - if err := ns.errorIfClosed(); err != nil { - return err - } - - if err := ns.file.Close(); err != nil { - return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) - } - ns.closed = true - - if ns.mounted { - if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil { - return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err) - } - if err := os.RemoveAll(ns.file.Name()); err != nil { - return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err) - } - ns.mounted = false - } - - return nil -} - func (ns *netNS) Do(toRun func(NetNS) error) error { if err := ns.errorIfClosed(); err != nil { return err @@ -281,18 +175,6 @@ func (ns *netNS) Do(toRun func(NetNS) error) error { return innerError } -func (ns *netNS) Set() error { - if err := ns.errorIfClosed(); err != nil { - return err - } - - if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 { - return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) - } - - return nil -} - // WithNetNSPath executes the passed closure under the given network // namespace, restoring the original namespace afterwards. func WithNetNSPath(nspath string, toRun func(NetNS) error) error { diff --git a/ns/ns_linux.go b/ns/ns_linux.go new file mode 100644 index 00000000..0746f159 --- /dev/null +++ b/ns/ns_linux.go @@ -0,0 +1,144 @@ +// Copyright 2015-2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ns + +import ( + "crypto/rand" + "fmt" + "os" + "path" + "runtime" + "sync" + + "golang.org/x/sys/unix" +) + +func getCurrentThreadNetNSPath() string { + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread this goroutine is running on. Make sure we + // use the thread's net namespace since the thread is switching around + return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) +} + +// Creates a new persistent network namespace and returns an object +// representing that namespace, without switching to it +func NewNS() (NetNS, error) { + const nsRunDir = "/var/run/netns" + + b := make([]byte, 16) + _, err := rand.Reader.Read(b) + if err != nil { + return nil, fmt.Errorf("failed to generate random netns name: %v", err) + } + + err = os.MkdirAll(nsRunDir, 0755) + if err != nil { + return nil, err + } + + // create an empty file at the mount point + nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) + nsPath := path.Join(nsRunDir, nsName) + mountPointFd, err := os.Create(nsPath) + if err != nil { + return nil, err + } + mountPointFd.Close() + + // Ensure the mount point is cleaned up on errors; if the namespace + // was successfully mounted this will have no effect because the file + // is in-use + defer os.RemoveAll(nsPath) + + var wg sync.WaitGroup + wg.Add(1) + + // do namespace work in a dedicated goroutine, so that we can safely + // Lock/Unlock OSThread without upsetting the lock/unlock state of + // the caller of this function + var fd *os.File + go (func() { + defer wg.Done() + runtime.LockOSThread() + + var origNS NetNS + origNS, err = GetNS(getCurrentThreadNetNSPath()) + if err != nil { + return + } + defer origNS.Close() + + // create a new netns on the current thread + err = unix.Unshare(unix.CLONE_NEWNET) + if err != nil { + return + } + defer origNS.Set() + + // bind mount the new netns from the current thread onto the mount point + err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") + if err != nil { + return + } + + fd, err = os.Open(nsPath) + if err != nil { + return + } + })() + wg.Wait() + + if err != nil { + unix.Unmount(nsPath, unix.MNT_DETACH) + return nil, fmt.Errorf("failed to create namespace: %v", err) + } + + return &netNS{file: fd, mounted: true}, nil +} + +func (ns *netNS) Close() error { + if err := ns.errorIfClosed(); err != nil { + return err + } + + if err := ns.file.Close(); err != nil { + return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) + } + ns.closed = true + + if ns.mounted { + if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil { + return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err) + } + if err := os.RemoveAll(ns.file.Name()); err != nil { + return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err) + } + ns.mounted = false + } + + return nil +} + +func (ns *netNS) Set() error { + if err := ns.errorIfClosed(); err != nil { + return err + } + + if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 { + return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) + } + + return nil +} diff --git a/ns/ns_unspecified.go b/ns/ns_unspecified.go new file mode 100644 index 00000000..da8b1796 --- /dev/null +++ b/ns/ns_unspecified.go @@ -0,0 +1,33 @@ +// Copyright 2015-2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !linux + +package ns + +func getCurrentThreadNetNSPath() string { + return "" +} + +func NewNS() (NetNS, error) { + return nil, NotImplementedError +} + +func (ns *netNS) Close() error { + return NotImplementedError +} + +func (ns *netNS) Set() error { + return NotImplementedError +} From 46deb7b708b2dcb11f861758122b28cf892238d0 Mon Sep 17 00:00:00 2001 From: Aithal Date: Thu, 23 Feb 2017 09:50:48 -0800 Subject: [PATCH 121/134] pkg/ns, pkg/types: refactored non linux build fix code to Make GetCurrentNS platform specific instead of getCurrentThreadNetNSPath --- ns/ns.go | 13 ++----------- ns/ns_linux.go | 5 +++++ ns/ns_unspecified.go | 13 ++++++++----- types/types.go | 4 ++++ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/ns/ns.go b/ns/ns.go index 428fb77f..c212f489 100644 --- a/ns/ns.go +++ b/ns/ns.go @@ -15,7 +15,6 @@ package ns import ( - "errors" "fmt" "os" "runtime" @@ -62,14 +61,6 @@ type netNS struct { // netNS implements the NetNS interface var _ NetNS = &netNS{} -// NotImplementedError is used to indicate that a method is not implemented for the given platform -var NotImplementedError = errors.New("Not Implemented") - -// Returns an object representing the current OS thread's network namespace -func GetCurrentNS() (NetNS, error) { - return GetNS(getCurrentThreadNetNSPath()) -} - const ( // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h NSFS_MAGIC = 0x6e736673 @@ -139,7 +130,7 @@ func (ns *netNS) Do(toRun func(NetNS) error) error { } containedCall := func(hostNS NetNS) error { - threadNS, err := GetNS(getCurrentThreadNetNSPath()) + threadNS, err := GetCurrentNS() if err != nil { return fmt.Errorf("failed to open current netns: %v", err) } @@ -155,7 +146,7 @@ func (ns *netNS) Do(toRun func(NetNS) error) error { } // save a handle to current network namespace - hostNS, err := GetNS(getCurrentThreadNetNSPath()) + hostNS, err := GetCurrentNS() if err != nil { return fmt.Errorf("Failed to open current namespace: %v", err) } diff --git a/ns/ns_linux.go b/ns/ns_linux.go index 0746f159..c9e1b4f0 100644 --- a/ns/ns_linux.go +++ b/ns/ns_linux.go @@ -25,6 +25,11 @@ import ( "golang.org/x/sys/unix" ) +// Returns an object representing the current OS thread's network namespace +func GetCurrentNS() (NetNS, error) { + return GetNS(getCurrentThreadNetNSPath()) +} + func getCurrentThreadNetNSPath() string { // /proc/self/ns/net returns the namespace of the main thread, not // of whatever thread this goroutine is running on. Make sure we diff --git a/ns/ns_unspecified.go b/ns/ns_unspecified.go index da8b1796..41b44686 100644 --- a/ns/ns_unspecified.go +++ b/ns/ns_unspecified.go @@ -16,18 +16,21 @@ package ns -func getCurrentThreadNetNSPath() string { - return "" +import "github.com/containernetworking/cni/pkg/types" + +// Returns an object representing the current OS thread's network namespace +func GetCurrentNS() (NetNS, error) { + return nil, types.NotImplementedError } func NewNS() (NetNS, error) { - return nil, NotImplementedError + return nil, types.NotImplementedError } func (ns *netNS) Close() error { - return NotImplementedError + return types.NotImplementedError } func (ns *netNS) Set() error { - return NotImplementedError + return types.NotImplementedError } diff --git a/types/types.go b/types/types.go index a81ac702..b7c27de1 100644 --- a/types/types.go +++ b/types/types.go @@ -16,6 +16,7 @@ package types import ( "encoding/json" + "errors" "fmt" "net" "os" @@ -178,3 +179,6 @@ func prettyPrint(obj interface{}) error { _, err = os.Stdout.Write(data) return err } + +// NotImplementedError is used to indicate that a method is not implemented for the given platform +var NotImplementedError = errors.New("Not Implemented") From 7d5619d27787acb55bb0cb477e49aff801bc618b Mon Sep 17 00:00:00 2001 From: Aithal Date: Thu, 23 Feb 2017 09:52:13 -0800 Subject: [PATCH 122/134] pkg/ip: refactored so that builds succeed on non-linux platforms moved functions that depend on linux packages into a separate file and added nop methods with a build tag for non-linux platforms in a new file. --- ip/route.go | 20 -------------------- ip/route_linux.go | 41 +++++++++++++++++++++++++++++++++++++++++ ip/route_unspecified.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 20 deletions(-) create mode 100644 ip/route_linux.go create mode 100644 ip/route_unspecified.go diff --git a/ip/route.go b/ip/route.go index 6c8658b2..1325a47a 100644 --- a/ip/route.go +++ b/ip/route.go @@ -25,23 +25,3 @@ func AddDefaultRoute(gw net.IP, dev netlink.Link) error { _, defNet, _ := net.ParseCIDR("0.0.0.0/0") return AddRoute(defNet, gw, dev) } - -// AddRoute adds a universally-scoped route to a device. -func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return netlink.RouteAdd(&netlink.Route{ - LinkIndex: dev.Attrs().Index, - Scope: netlink.SCOPE_UNIVERSE, - Dst: ipn, - Gw: gw, - }) -} - -// AddHostRoute adds a host-scoped route to a device. -func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return netlink.RouteAdd(&netlink.Route{ - LinkIndex: dev.Attrs().Index, - Scope: netlink.SCOPE_HOST, - Dst: ipn, - Gw: gw, - }) -} diff --git a/ip/route_linux.go b/ip/route_linux.go new file mode 100644 index 00000000..8b11807d --- /dev/null +++ b/ip/route_linux.go @@ -0,0 +1,41 @@ +// Copyright 2015-2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip + +import ( + "net" + + "github.com/vishvananda/netlink" +) + +// AddRoute adds a universally-scoped route to a device. +func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_UNIVERSE, + Dst: ipn, + Gw: gw, + }) +} + +// AddHostRoute adds a host-scoped route to a device. +func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_HOST, + Dst: ipn, + Gw: gw, + }) +} diff --git a/ip/route_unspecified.go b/ip/route_unspecified.go new file mode 100644 index 00000000..7e79fdef --- /dev/null +++ b/ip/route_unspecified.go @@ -0,0 +1,34 @@ +// Copyright 2015-2017 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !linux + +package ip + +import ( + "net" + + "github.com/containernetworking/cni/pkg/types" + "github.com/vishvananda/netlink" +) + +// AddRoute adds a universally-scoped route to a device. +func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return types.NotImplementedError +} + +// AddHostRoute adds a host-scoped route to a device. +func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return types.NotImplementedError +} From 2a6d12b2069e8b8e2523c502d975114260571200 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Mon, 27 Feb 2017 13:27:19 -0600 Subject: [PATCH 123/134] pkg/testutils: return errors after restoring stdout Ensures Ginkgo is able to print detailed failure messages instead of them being captured by the pipe. --- testutils/cmd.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/testutils/cmd.go b/testutils/cmd.go index 5883c08f..a6045b31 100644 --- a/testutils/cmd.go +++ b/testutils/cmd.go @@ -46,13 +46,15 @@ func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) ( os.Stdout = w err = f() w.Close() - if err != nil { - return nil, nil, err - } - // parse the result - out, err := ioutil.ReadAll(r) + var out []byte + if err == nil { + out, err = ioutil.ReadAll(r) + } os.Stdout = oldStdout + + // Return errors after restoring stdout so Ginkgo will correctly + // emit verbose error information on stdout if err != nil { return nil, nil, err } From 2f1bfcee63813ee5fb6fdd083a80b54a0ffd1d14 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Mon, 27 Feb 2017 13:28:07 -0600 Subject: [PATCH 124/134] pkg/types: misc current types testcase cleanups --- types/current/types_suite_test.go | 4 ++-- types/current/types_test.go | 38 +++++++++++++++---------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/types/current/types_suite_test.go b/types/current/types_suite_test.go index 89cccecd..e05c1ff1 100644 --- a/types/current/types_suite_test.go +++ b/types/current/types_suite_test.go @@ -21,7 +21,7 @@ import ( "testing" ) -func TestTypes010(t *testing.T) { +func TestTypesCurrent(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "0.3.0 Types Suite") + RunSpecs(t, "Current Types Suite") } diff --git a/types/current/types_test.go b/types/current/types_test.go index f839cc90..c5012752 100644 --- a/types/current/types_test.go +++ b/types/current/types_test.go @@ -81,7 +81,7 @@ func testResult() *current.Result { } } -var _ = Describe("Ensures compatibility with the 0.3.0 spec", func() { +var _ = Describe("Current types operations", func() { It("correctly encodes a 0.3.0 Result", func() { res := testResult() @@ -150,29 +150,28 @@ var _ = Describe("Ensures compatibility with the 0.3.0 spec", func() { }`)) }) - var _ = Describe("Ensures compatibility with the 0.1.0 spec", func() { - It("correctly encodes a 0.1.0 Result", func() { - res, err := testResult().GetAsVersion("0.1.0") - Expect(err).NotTo(HaveOccurred()) + It("correctly encodes a 0.1.0 Result", func() { + res, err := testResult().GetAsVersion("0.1.0") + Expect(err).NotTo(HaveOccurred()) - Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) + Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) - // Redirect stdout to capture JSON result - oldStdout := os.Stdout - r, w, err := os.Pipe() - Expect(err).NotTo(HaveOccurred()) + // Redirect stdout to capture JSON result + oldStdout := os.Stdout + r, w, err := os.Pipe() + Expect(err).NotTo(HaveOccurred()) - os.Stdout = w - err = res.Print() - w.Close() - Expect(err).NotTo(HaveOccurred()) + os.Stdout = w + err = res.Print() + w.Close() + Expect(err).NotTo(HaveOccurred()) - // parse the result - out, err := ioutil.ReadAll(r) - os.Stdout = oldStdout - Expect(err).NotTo(HaveOccurred()) + // parse the result + out, err := ioutil.ReadAll(r) + os.Stdout = oldStdout + Expect(err).NotTo(HaveOccurred()) - Expect(string(out)).To(Equal(`{ + Expect(string(out)).To(Equal(`{ "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", @@ -209,6 +208,5 @@ var _ = Describe("Ensures compatibility with the 0.3.0 spec", func() { ] } }`)) - }) }) }) From 9b8a0e0c6478413b01d846d1292e05627c008819 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 16 Feb 2017 22:57:12 -0600 Subject: [PATCH 125/134] spec,libcni: add support for injecting runtimeConfig into plugin stdin data Add a new CapabilityArgs member to the RuntimeConf struct which runtimes can use to pass arbitrary capability-based keys to the plugin. Elements of this member will be filtered against the plugin's advertised capabilities (from its config JSON) and then added to a new "runtimeConfig" top-level map added to the config JSON sent to the plugin on stdin. Also "runtime_config"->"runtimeConfig" in CONVENTIONS.md to make capitalization consistent with other CNI config keys like "cniVersion". --- types/types.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/types/types.go b/types/types.go index b7c27de1..3263015a 100644 --- a/types/types.go +++ b/types/types.go @@ -60,9 +60,10 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - IPAM struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Capabilities map[string]bool `json:"capabilities,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"` From b07d768f12b80671aafd48ff5f560d3c2a32dc7a Mon Sep 17 00:00:00 2001 From: Jay Dunkelberger Date: Thu, 9 Mar 2017 13:55:15 -0800 Subject: [PATCH 126/134] pkg/ip: do not leak types from vendored netlink package The exported function SetupVeth now returns a package-defined type. Signed-off-by: Gabe Rosenhouse --- ip/link.go | 46 ++++++++++++++++++++++++++++++++++------------ ip/link_test.go | 4 ++-- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/ip/link.go b/ip/link.go index 6431bb41..8900f653 100644 --- a/ip/link.go +++ b/ip/link.go @@ -98,30 +98,49 @@ func RenameLink(curName, newName string) error { return err } +type LinkAttrs struct { + Name string + HardwareAddr net.HardwareAddr + Index int +} + +type link struct { + netlink.Link +} + +func (l *link) Attrs() LinkAttrs { + a := l.Link.Attrs() + return LinkAttrs{ + Name: a.Name, + HardwareAddr: a.HardwareAddr, + Index: a.Index, + } +} + +type Link interface { + Attrs() LinkAttrs +} + // SetupVeth sets up a virtual ethernet link. // Should be in container netns, and will switch back to hostNS to set the host // veth end up. -func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVeth netlink.Link, err error) { - var hostVethName string - hostVethName, contVeth, err = makeVeth(contVethName, mtu) +func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (Link, Link, error) { + hostVethName, contVeth, err := makeVeth(contVethName, mtu) if err != nil { - return + return nil, nil, err } if err = netlink.LinkSetUp(contVeth); err != nil { - err = fmt.Errorf("failed to set %q up: %v", contVethName, err) - return + return nil, nil, fmt.Errorf("failed to set %q up: %v", contVethName, err) } - hostVeth, err = netlink.LinkByName(hostVethName) + hostVeth, err := netlink.LinkByName(hostVethName) if err != nil { - err = fmt.Errorf("failed to lookup %q: %v", hostVethName, err) - return + return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) } if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { - err = fmt.Errorf("failed to move veth to host netns: %v", err) - return + return nil, nil, fmt.Errorf("failed to move veth to host netns: %v", err) } err = hostNS.Do(func(_ ns.NetNS) error { @@ -135,7 +154,10 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (hostVeth, contVet } return nil }) - return + if err != nil { + return nil, nil, err + } + return &link{hostVeth}, &link{contVeth}, nil } // DelLinkByName removes an interface link. diff --git a/ip/link_test.go b/ip/link_test.go index 3df9ab8f..2c9ae400 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -46,8 +46,8 @@ var _ = Describe("Link", func() { hostNetNS ns.NetNS containerNetNS ns.NetNS ifaceCounter int = 0 - hostVeth netlink.Link - containerVeth netlink.Link + hostVeth ip.Link + containerVeth ip.Link hostVethName string containerVethName string From 71d96ea91c803363586ae79cdac87eadd277521e Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Fri, 10 Mar 2017 09:49:46 -0800 Subject: [PATCH 127/134] pkg/ip: SetupVeth returns net.Interface --- ip/link.go | 38 +++++++++++++------------------------- ip/link_test.go | 14 +++++++------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/ip/link.go b/ip/link.go index 8900f653..52f6aae2 100644 --- a/ip/link.go +++ b/ip/link.go @@ -98,49 +98,37 @@ func RenameLink(curName, newName string) error { return err } -type LinkAttrs struct { - Name string - HardwareAddr net.HardwareAddr - Index int -} - -type link struct { - netlink.Link -} - -func (l *link) Attrs() LinkAttrs { - a := l.Link.Attrs() - return LinkAttrs{ +func ifaceFromNetlinkLink(l netlink.Link) net.Interface { + a := l.Attrs() + return net.Interface{ + Index: a.Index, + MTU: a.MTU, Name: a.Name, HardwareAddr: a.HardwareAddr, - Index: a.Index, + Flags: a.Flags, } } -type Link interface { - Attrs() LinkAttrs -} - // SetupVeth sets up a virtual ethernet link. // Should be in container netns, and will switch back to hostNS to set the host // veth end up. -func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (Link, Link, error) { +func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) { hostVethName, contVeth, err := makeVeth(contVethName, mtu) if err != nil { - return nil, nil, err + return net.Interface{}, net.Interface{}, err } if err = netlink.LinkSetUp(contVeth); err != nil { - return nil, nil, fmt.Errorf("failed to set %q up: %v", contVethName, err) + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err) } hostVeth, err := netlink.LinkByName(hostVethName) if err != nil { - return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) } if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { - return nil, nil, fmt.Errorf("failed to move veth to host netns: %v", err) + return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err) } err = hostNS.Do(func(_ ns.NetNS) error { @@ -155,9 +143,9 @@ func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (Link, Link, error return nil }) if err != nil { - return nil, nil, err + return net.Interface{}, net.Interface{}, err } - return &link{hostVeth}, &link{contVeth}, nil + return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil } // DelLinkByName removes an interface link. diff --git a/ip/link_test.go b/ip/link_test.go index 2c9ae400..85d8b944 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -46,8 +46,8 @@ var _ = Describe("Link", func() { hostNetNS ns.NetNS containerNetNS ns.NetNS ifaceCounter int = 0 - hostVeth ip.Link - containerVeth ip.Link + hostVeth net.Interface + containerVeth net.Interface hostVethName string containerVethName string @@ -78,8 +78,8 @@ var _ = Describe("Link", func() { } Expect(err).NotTo(HaveOccurred()) - hostVethName = hostVeth.Attrs().Name - containerVethName = containerVeth.Attrs().Name + hostVethName = hostVeth.Name + containerVethName = containerVeth.Name return nil }) @@ -98,7 +98,7 @@ var _ = Describe("Link", func() { containerVethFromName, err := netlink.LinkByName(containerVethName) Expect(err).NotTo(HaveOccurred()) - Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Attrs().Index)) + Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Index)) return nil }) @@ -108,7 +108,7 @@ var _ = Describe("Link", func() { hostVethFromName, err := netlink.LinkByName(hostVethName) Expect(err).NotTo(HaveOccurred()) - Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Attrs().Index)) + Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Index)) return nil }) @@ -156,7 +156,7 @@ var _ = Describe("Link", func() { hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS) Expect(err).NotTo(HaveOccurred()) - hostVethName = hostVeth.Attrs().Name + hostVethName = hostVeth.Name return nil }) From bb322572eb6913e3b2fc537f22ac85ae2c47b94c Mon Sep 17 00:00:00 2001 From: Gabe Rosenhouse Date: Mon, 13 Mar 2017 11:27:12 -0700 Subject: [PATCH 128/134] pkg/ip: improve docstring for SetupVeth --- ip/link.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ip/link.go b/ip/link.go index 52f6aae2..ff0bdb29 100644 --- a/ip/link.go +++ b/ip/link.go @@ -109,9 +109,10 @@ func ifaceFromNetlinkLink(l netlink.Link) net.Interface { } } -// SetupVeth sets up a virtual ethernet link. -// Should be in container netns, and will switch back to hostNS to set the host -// veth end up. +// SetupVeth sets up a pair of virtual ethernet devices. +// Call SetupVeth from inside the container netns. It will create both veth +// devices and move the host-side veth into the provided hostNS namespace. +// On success, SetupVeth returns (hostVeth, containerVeth, nil) func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) { hostVethName, contVeth, err := makeVeth(contVethName, mtu) if err != nil { From 58b78e72fd390a2cafcd3b6b71a0aed4e75f897d Mon Sep 17 00:00:00 2001 From: Tom Denham Date: Mon, 20 Mar 2017 15:49:35 -0700 Subject: [PATCH 129/134] plugins/*: Don't error if the device doesn't exist I wasn't able to test or update the dhcp plugin but from a code read it should be fine. All the other plugins are tested and fixed --- ip/link.go | 8 ++++++++ ip/link_test.go | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/ip/link.go b/ip/link.go index ff0bdb29..a9842627 100644 --- a/ip/link.go +++ b/ip/link.go @@ -16,6 +16,7 @@ package ip import ( "crypto/rand" + "errors" "fmt" "net" "os" @@ -25,6 +26,10 @@ import ( "github.com/vishvananda/netlink" ) +var ( + ErrLinkNotFound = errors.New("link not found") +) + func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { veth := &netlink.Veth{ LinkAttrs: netlink.LinkAttrs{ @@ -168,6 +173,9 @@ func DelLinkByName(ifName string) error { func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { iface, err := netlink.LinkByName(ifName) if err != nil { + if err != nil && err.Error() == "Link not found" { + return nil, ErrLinkNotFound + } return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) } diff --git a/ip/link_test.go b/ip/link_test.go index 85d8b944..23182a54 100644 --- a/ip/link_test.go +++ b/ip/link_test.go @@ -127,6 +127,20 @@ var _ = Describe("Link", func() { }) }) + Context("deleting an non-existent device", func() { + It("returns known error", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // This string should match the expected error codes in the cmdDel functions of some of the plugins + _, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST", netlink.FAMILY_V4) + Expect(err).To(Equal(ip.ErrLinkNotFound)) + + return nil + }) + }) + }) + Context("when there is no name available for the host-side", func() { BeforeEach(func() { //adding different interface to container ns From 29ba54c045f272b828fe791ec7845c1af95861ef Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Wed, 22 Mar 2017 14:21:53 -0500 Subject: [PATCH 130/134] spec/plugins: fix 'ip'->'ips' in the spec, bump to 0.3.1 --- invoke/exec_test.go | 2 +- invoke/raw_exec_test.go | 2 +- skel/skel_test.go | 4 ++-- types/current/types.go | 10 +++++----- types/current/types_test.go | 2 +- version/version.go | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/invoke/exec_test.go b/invoke/exec_test.go index 7e804ab7..33ffc2de 100644 --- a/invoke/exec_test.go +++ b/invoke/exec_test.go @@ -50,7 +50,7 @@ var _ = Describe("Executing a plugin, unit tests", func() { VersionDecoder: versionDecoder, } pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.0" }`) + netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.1" }`) cniargs = &fakes.CNIArgs{} cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} }) diff --git a/invoke/raw_exec_test.go b/invoke/raw_exec_test.go index 5ab23ae5..5d759f24 100644 --- a/invoke/raw_exec_test.go +++ b/invoke/raw_exec_test.go @@ -58,7 +58,7 @@ var _ = Describe("RawExec", func() { "CNI_PATH=/some/bin/path", "CNI_IFNAME=some-eth0", } - stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.0"}`) + stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.1"}`) execer = &invoke.RawExec{} }) diff --git a/skel/skel_test.go b/skel/skel_test.go index d7f729f0..ad293084 100644 --- a/skel/skel_test.go +++ b/skel/skel_test.go @@ -226,7 +226,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.3.0", + "cniVersion": "0.3.1", "supportedVersions": ["9.8.7"] }`)) }) @@ -258,7 +258,7 @@ var _ = Describe("dispatching to the correct callback", func() { Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.3.0", + "cniVersion": "0.3.1", "supportedVersions": ["9.8.7"] }`)) }) diff --git a/types/current/types.go b/types/current/types.go index e686a9a7..b89a5d3a 100644 --- a/types/current/types.go +++ b/types/current/types.go @@ -24,9 +24,9 @@ import ( "github.com/containernetworking/cni/pkg/types/020" ) -const implementedSpecVersion string = "0.3.0" +const implementedSpecVersion string = "0.3.1" -var SupportedVersions = []string{implementedSpecVersion} +var SupportedVersions = []string{"0.3.0", implementedSpecVersion} func NewResult(data []byte) (types.Result, error) { result := &Result{} @@ -129,7 +129,7 @@ func NewResultFromResult(result types.Result) (*Result, error) { } } } - return nil, fmt.Errorf("unsupported CNI result version %q", version) + return nil, fmt.Errorf("unsupported CNI result22 version %q", version) } // Result is what gets returned from the plugin (via stdout) to the caller @@ -194,12 +194,12 @@ func (r *Result) Version() string { func (r *Result) GetAsVersion(version string) (types.Result, error) { switch version { - case implementedSpecVersion: + case "0.3.0", implementedSpecVersion: return r, nil case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: return r.convertTo020() } - return nil, fmt.Errorf("cannot convert version 0.3.0 to %q", version) + return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version) } func (r *Result) Print() error { diff --git a/types/current/types_test.go b/types/current/types_test.go index c5012752..9d29cf22 100644 --- a/types/current/types_test.go +++ b/types/current/types_test.go @@ -82,7 +82,7 @@ func testResult() *current.Result { } var _ = Describe("Current types operations", func() { - It("correctly encodes a 0.3.0 Result", func() { + It("correctly encodes a 0.3.x Result", func() { res := testResult() Expect(res.String()).To(Equal("Interfaces:[{Name:eth0 Mac:00:11:22:33:44:55 Sandbox:/proc/3553/ns/net}], IP:[{Version:4 Interface:0 Address:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1} {Version:6 Interface:0 Address:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1}], Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8} {Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}], DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) diff --git a/version/version.go b/version/version.go index 7c589633..efe8ea87 100644 --- a/version/version.go +++ b/version/version.go @@ -24,7 +24,7 @@ import ( // Current reports the version of the CNI spec implemented by this library func Current() string { - return "0.3.0" + return "0.3.1" } // Legacy PluginInfo describes a plugin that is backwards compatible with the @@ -35,7 +35,7 @@ func Current() string { // Any future CNI spec versions which meet this definition should be added to // this list. var Legacy = PluginSupports("0.1.0", "0.2.0") -var All = PluginSupports("0.1.0", "0.2.0", "0.3.0") +var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1") var resultFactories = []struct { supportedVersions []string From 3cd4e26aebe30d24e245991df6cc2687c1784584 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 3 May 2017 21:38:28 +0800 Subject: [PATCH 131/134] Add cniVersion to Result Signed-off-by: Pengfei Ni --- types/020/types.go | 16 +++++++++------- types/020/types_test.go | 2 ++ types/current/types.go | 11 ++++++++--- types/current/types_test.go | 3 +++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/types/020/types.go b/types/020/types.go index 666cfe93..2833aba7 100644 --- a/types/020/types.go +++ b/types/020/types.go @@ -23,9 +23,9 @@ import ( "github.com/containernetworking/cni/pkg/types" ) -const implementedSpecVersion string = "0.2.0" +const ImplementedSpecVersion string = "0.2.0" -var SupportedVersions = []string{"", "0.1.0", implementedSpecVersion} +var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion} // Compatibility types for CNI version 0.1.0 and 0.2.0 @@ -39,7 +39,7 @@ func NewResult(data []byte) (types.Result, error) { func GetResult(r types.Result) (*Result, error) { // We expect version 0.1.0/0.2.0 results - result020, err := r.GetAsVersion(implementedSpecVersion) + result020, err := r.GetAsVersion(ImplementedSpecVersion) if err != nil { return nil, err } @@ -52,18 +52,20 @@ func GetResult(r types.Result) (*Result, error) { // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { - IP4 *IPConfig `json:"ip4,omitempty"` - IP6 *IPConfig `json:"ip6,omitempty"` - DNS types.DNS `json:"dns,omitempty"` + CNIVersion string `json:"cniVersion,omitempty"` + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` + DNS types.DNS `json:"dns,omitempty"` } func (r *Result) Version() string { - return implementedSpecVersion + return ImplementedSpecVersion } func (r *Result) GetAsVersion(version string) (types.Result, error) { for _, supportedVersion := range SupportedVersions { if version == supportedVersion { + r.CNIVersion = version return r, nil } } diff --git a/types/020/types_test.go b/types/020/types_test.go index 1bcdda73..4f08ca49 100644 --- a/types/020/types_test.go +++ b/types/020/types_test.go @@ -48,6 +48,7 @@ var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() { // Set every field of the struct to ensure source compatibility res := types020.Result{ + CNIVersion: types020.ImplementedSpecVersion, IP4: &types020.IPConfig{ IP: *ipv4, Gateway: net.ParseIP("1.2.3.1"), @@ -88,6 +89,7 @@ var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ + "cniVersion": "0.2.0", "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", diff --git a/types/current/types.go b/types/current/types.go index b89a5d3a..26e1dd96 100644 --- a/types/current/types.go +++ b/types/current/types.go @@ -63,8 +63,9 @@ func convertFrom020(result types.Result) (*Result, error) { } newResult := &Result{ - DNS: oldResult.DNS, - Routes: []*types.Route{}, + CNIVersion: implementedSpecVersion, + DNS: oldResult.DNS, + Routes: []*types.Route{}, } if oldResult.IP4 != nil { @@ -117,6 +118,7 @@ func convertFrom030(result types.Result) (*Result, error) { if !ok { return nil, fmt.Errorf("failed to convert result") } + newResult.CNIVersion = implementedSpecVersion return newResult, nil } @@ -134,6 +136,7 @@ func NewResultFromResult(result types.Result) (*Result, error) { // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { + CNIVersion string `json:"cniVersion,omitempty"` Interfaces []*Interface `json:"interfaces,omitempty"` IPs []*IPConfig `json:"ips,omitempty"` Routes []*types.Route `json:"routes,omitempty"` @@ -143,7 +146,8 @@ type Result struct { // Convert to the older 0.2.0 CNI spec Result type func (r *Result) convertTo020() (*types020.Result, error) { oldResult := &types020.Result{ - DNS: r.DNS, + CNIVersion: types020.ImplementedSpecVersion, + DNS: r.DNS, } for _, ip := range r.IPs { @@ -195,6 +199,7 @@ func (r *Result) Version() string { func (r *Result) GetAsVersion(version string) (types.Result, error) { switch version { case "0.3.0", implementedSpecVersion: + r.CNIVersion = version return r, nil case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: return r.convertTo020() diff --git a/types/current/types_test.go b/types/current/types_test.go index 9d29cf22..eb7e7c62 100644 --- a/types/current/types_test.go +++ b/types/current/types_test.go @@ -47,6 +47,7 @@ func testResult() *current.Result { // Set every field of the struct to ensure source compatibility return ¤t.Result{ + CNIVersion: "0.3.1", Interfaces: []*current.Interface{ { Name: "eth0", @@ -103,6 +104,7 @@ var _ = Describe("Current types operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ + "cniVersion": "0.3.1", "interfaces": [ { "name": "eth0", @@ -172,6 +174,7 @@ var _ = Describe("Current types operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ + "cniVersion": "0.1.0", "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", From a11be4d7596203874b742b6597caf255204c56c3 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Wed, 3 May 2017 23:21:44 +0800 Subject: [PATCH 132/134] Fix testings accross the project Signed-off-by: Pengfei Ni --- types/current/types.go | 14 +++++++------- types/current/types_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/types/current/types.go b/types/current/types.go index 26e1dd96..b5715fe6 100644 --- a/types/current/types.go +++ b/types/current/types.go @@ -24,9 +24,9 @@ import ( "github.com/containernetworking/cni/pkg/types/020" ) -const implementedSpecVersion string = "0.3.1" +const ImplementedSpecVersion string = "0.3.1" -var SupportedVersions = []string{"0.3.0", implementedSpecVersion} +var SupportedVersions = []string{"0.3.0", ImplementedSpecVersion} func NewResult(data []byte) (types.Result, error) { result := &Result{} @@ -37,7 +37,7 @@ func NewResult(data []byte) (types.Result, error) { } func GetResult(r types.Result) (*Result, error) { - resultCurrent, err := r.GetAsVersion(implementedSpecVersion) + resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func convertFrom020(result types.Result) (*Result, error) { } newResult := &Result{ - CNIVersion: implementedSpecVersion, + CNIVersion: ImplementedSpecVersion, DNS: oldResult.DNS, Routes: []*types.Route{}, } @@ -118,7 +118,7 @@ func convertFrom030(result types.Result) (*Result, error) { if !ok { return nil, fmt.Errorf("failed to convert result") } - newResult.CNIVersion = implementedSpecVersion + newResult.CNIVersion = ImplementedSpecVersion return newResult, nil } @@ -193,12 +193,12 @@ func (r *Result) convertTo020() (*types020.Result, error) { } func (r *Result) Version() string { - return implementedSpecVersion + return ImplementedSpecVersion } func (r *Result) GetAsVersion(version string) (types.Result, error) { switch version { - case "0.3.0", implementedSpecVersion: + case "0.3.0", ImplementedSpecVersion: r.CNIVersion = version return r, nil case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: diff --git a/types/current/types_test.go b/types/current/types_test.go index eb7e7c62..afc68670 100644 --- a/types/current/types_test.go +++ b/types/current/types_test.go @@ -174,7 +174,7 @@ var _ = Describe("Current types operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(out)).To(Equal(`{ - "cniVersion": "0.1.0", + "cniVersion": "0.2.0", "ip4": { "ip": "1.2.3.30/24", "gateway": "1.2.3.1", From 3c436520bed00a7bb19fb59b815aed89f9814b64 Mon Sep 17 00:00:00 2001 From: Casey Callendrello Date: Mon, 22 May 2017 20:09:42 +0200 Subject: [PATCH 133/134] move most of cni/pkg to plugins/pkg: delete code staying in cni/pkg This moves the following packages to this repository: * ip * ipam * ns * testutils * utils --- pkg/invoke/args.go | 79 ---- pkg/invoke/delegate.go | 53 --- pkg/invoke/exec.go | 95 ----- pkg/invoke/exec_test.go | 157 -------- pkg/invoke/fakes/cni_args.go | 27 -- pkg/invoke/fakes/raw_exec.go | 36 -- pkg/invoke/fakes/version_decoder.go | 34 -- pkg/invoke/find.go | 43 --- pkg/invoke/find_test.go | 103 ------ pkg/invoke/get_version_integration_test.go | 107 ------ pkg/invoke/invoke_suite_test.go | 45 --- pkg/invoke/os_unix.go | 20 - pkg/invoke/os_windows.go | 18 - pkg/invoke/raw_exec.go | 63 ---- pkg/invoke/raw_exec_test.go | 123 ------- pkg/ip/link.go | 4 +- pkg/ip/link_test.go | 4 +- pkg/ipam/ipam.go | 2 +- pkg/ipam/ipam_test.go | 2 +- pkg/ns/ns_test.go | 2 +- pkg/skel/skel.go | 228 ------------ pkg/skel/skel_suite_test.go | 27 -- pkg/skel/skel_test.go | 346 ------------------ pkg/types/020/types.go | 135 ------- pkg/types/020/types_suite_test.go | 27 -- pkg/types/020/types_test.go | 130 ------- pkg/types/args.go | 101 ----- pkg/types/args_test.go | 121 ------ pkg/types/current/types.go | 296 --------------- pkg/types/current/types_suite_test.go | 27 -- pkg/types/current/types_test.go | 215 ----------- pkg/types/types.go | 185 ---------- pkg/types/types_suite_test.go | 27 -- pkg/utils/hwaddr/hwaddr_test.go | 2 +- pkg/version/conf.go | 37 -- pkg/version/conf_test.go | 69 ---- .../legacy_examples/example_runtime.go | 167 --------- pkg/version/legacy_examples/examples.go | 139 ------- .../legacy_examples_suite_test.go | 27 -- .../legacy_examples/legacy_examples_test.go | 36 -- pkg/version/plugin.go | 81 ---- pkg/version/plugin_test.go | 85 ----- pkg/version/reconcile.go | 49 --- pkg/version/reconcile_test.go | 51 --- pkg/version/testhelpers/testhelpers.go | 156 -------- .../testhelpers/testhelpers_suite_test.go | 27 -- pkg/version/testhelpers/testhelpers_test.go | 106 ------ pkg/version/version.go | 61 --- pkg/version/version_suite_test.go | 27 -- plugins/ipam/dhcp/lease.go | 2 +- .../host-local/backend/allocator/allocator.go | 2 +- plugins/ipam/host-local/host_local_test.go | 2 +- plugins/main/bridge/bridge.go | 8 +- plugins/main/bridge/bridge_test.go | 7 +- plugins/main/ipvlan/ipvlan.go | 6 +- plugins/main/ipvlan/ipvlan_test.go | 4 +- plugins/main/loopback/loopback.go | 2 +- plugins/main/loopback/loopback_test.go | 2 +- plugins/main/macvlan/macvlan.go | 8 +- plugins/main/macvlan/macvlan_test.go | 6 +- plugins/main/ptp/ptp.go | 8 +- plugins/main/ptp/ptp_test.go | 4 +- plugins/main/vlan/vlan.go | 6 +- plugins/main/vlan/vlan_test.go | 4 +- plugins/meta/flannel/flannel_test.go | 4 +- plugins/meta/tuning/tuning.go | 2 +- plugins/sample/sample_test.go | 4 +- test.sh | 2 +- 68 files changed, 49 insertions(+), 4036 deletions(-) delete mode 100644 pkg/invoke/args.go delete mode 100644 pkg/invoke/delegate.go delete mode 100644 pkg/invoke/exec.go delete mode 100644 pkg/invoke/exec_test.go delete mode 100644 pkg/invoke/fakes/cni_args.go delete mode 100644 pkg/invoke/fakes/raw_exec.go delete mode 100644 pkg/invoke/fakes/version_decoder.go delete mode 100644 pkg/invoke/find.go delete mode 100644 pkg/invoke/find_test.go delete mode 100644 pkg/invoke/get_version_integration_test.go delete mode 100644 pkg/invoke/invoke_suite_test.go delete mode 100644 pkg/invoke/os_unix.go delete mode 100644 pkg/invoke/os_windows.go delete mode 100644 pkg/invoke/raw_exec.go delete mode 100644 pkg/invoke/raw_exec_test.go delete mode 100644 pkg/skel/skel.go delete mode 100644 pkg/skel/skel_suite_test.go delete mode 100644 pkg/skel/skel_test.go delete mode 100644 pkg/types/020/types.go delete mode 100644 pkg/types/020/types_suite_test.go delete mode 100644 pkg/types/020/types_test.go delete mode 100644 pkg/types/args.go delete mode 100644 pkg/types/args_test.go delete mode 100644 pkg/types/current/types.go delete mode 100644 pkg/types/current/types_suite_test.go delete mode 100644 pkg/types/current/types_test.go delete mode 100644 pkg/types/types.go delete mode 100644 pkg/types/types_suite_test.go delete mode 100644 pkg/version/conf.go delete mode 100644 pkg/version/conf_test.go delete mode 100644 pkg/version/legacy_examples/example_runtime.go delete mode 100644 pkg/version/legacy_examples/examples.go delete mode 100644 pkg/version/legacy_examples/legacy_examples_suite_test.go delete mode 100644 pkg/version/legacy_examples/legacy_examples_test.go delete mode 100644 pkg/version/plugin.go delete mode 100644 pkg/version/plugin_test.go delete mode 100644 pkg/version/reconcile.go delete mode 100644 pkg/version/reconcile_test.go delete mode 100644 pkg/version/testhelpers/testhelpers.go delete mode 100644 pkg/version/testhelpers/testhelpers_suite_test.go delete mode 100644 pkg/version/testhelpers/testhelpers_test.go delete mode 100644 pkg/version/version.go delete mode 100644 pkg/version/version_suite_test.go diff --git a/pkg/invoke/args.go b/pkg/invoke/args.go deleted file mode 100644 index ba9d0c3b..00000000 --- a/pkg/invoke/args.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -import ( - "os" - "strings" -) - -type CNIArgs interface { - // For use with os/exec; i.e., return nil to inherit the - // environment from this process - AsEnv() []string -} - -type inherited struct{} - -var inheritArgsFromEnv inherited - -func (_ *inherited) AsEnv() []string { - return nil -} - -func ArgsFromEnv() CNIArgs { - return &inheritArgsFromEnv -} - -type Args struct { - Command string - ContainerID string - NetNS string - PluginArgs [][2]string - PluginArgsStr string - IfName string - Path string -} - -// Args implements the CNIArgs interface -var _ CNIArgs = &Args{} - -func (args *Args) AsEnv() []string { - env := os.Environ() - pluginArgsStr := args.PluginArgsStr - if pluginArgsStr == "" { - pluginArgsStr = stringify(args.PluginArgs) - } - - env = append(env, - "CNI_COMMAND="+args.Command, - "CNI_CONTAINERID="+args.ContainerID, - "CNI_NETNS="+args.NetNS, - "CNI_ARGS="+pluginArgsStr, - "CNI_IFNAME="+args.IfName, - "CNI_PATH="+args.Path) - return env -} - -// taken from rkt/networking/net_plugin.go -func stringify(pluginArgs [][2]string) string { - entries := make([]string, len(pluginArgs)) - - for i, kv := range pluginArgs { - entries[i] = strings.Join(kv[:], "=") - } - - return strings.Join(entries, ";") -} diff --git a/pkg/invoke/delegate.go b/pkg/invoke/delegate.go deleted file mode 100644 index c78a69ee..00000000 --- a/pkg/invoke/delegate.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/containernetworking/cni/pkg/types" -) - -func DelegateAdd(delegatePlugin string, netconf []byte) (types.Result, error) { - if os.Getenv("CNI_COMMAND") != "ADD" { - return nil, fmt.Errorf("CNI_COMMAND is not ADD") - } - - paths := filepath.SplitList(os.Getenv("CNI_PATH")) - - pluginPath, err := FindInPath(delegatePlugin, paths) - if err != nil { - return nil, err - } - - return ExecPluginWithResult(pluginPath, netconf, ArgsFromEnv()) -} - -func DelegateDel(delegatePlugin string, netconf []byte) error { - if os.Getenv("CNI_COMMAND") != "DEL" { - return fmt.Errorf("CNI_COMMAND is not DEL") - } - - paths := filepath.SplitList(os.Getenv("CNI_PATH")) - - pluginPath, err := FindInPath(delegatePlugin, paths) - if err != nil { - return err - } - - return ExecPluginWithoutResult(pluginPath, netconf, ArgsFromEnv()) -} diff --git a/pkg/invoke/exec.go b/pkg/invoke/exec.go deleted file mode 100644 index fc47e7c8..00000000 --- a/pkg/invoke/exec.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -import ( - "fmt" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/version" -) - -func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { - return defaultPluginExec.WithResult(pluginPath, netconf, args) -} - -func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { - return defaultPluginExec.WithoutResult(pluginPath, netconf, args) -} - -func GetVersionInfo(pluginPath string) (version.PluginInfo, error) { - return defaultPluginExec.GetVersionInfo(pluginPath) -} - -var defaultPluginExec = &PluginExec{ - RawExec: &RawExec{Stderr: os.Stderr}, - VersionDecoder: &version.PluginDecoder{}, -} - -type PluginExec struct { - RawExec interface { - ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) - } - VersionDecoder interface { - Decode(jsonBytes []byte) (version.PluginInfo, error) - } -} - -func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) { - stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) - if err != nil { - return nil, err - } - - // Plugin must return result in same version as specified in netconf - versionDecoder := &version.ConfigDecoder{} - confVersion, err := versionDecoder.Decode(netconf) - if err != nil { - return nil, err - } - - return version.NewResult(confVersion, stdoutBytes) -} - -func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error { - _, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv()) - return err -} - -// GetVersionInfo returns the version information available about the plugin. -// For recent-enough plugins, it uses the information returned by the VERSION -// command. For older plugins which do not recognize that command, it reports -// version 0.1.0 -func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) { - args := &Args{ - Command: "VERSION", - - // set fake values required by plugins built against an older version of skel - NetNS: "dummy", - IfName: "dummy", - Path: "dummy", - } - stdin := []byte(fmt.Sprintf(`{"cniVersion":%q}`, version.Current())) - stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, stdin, args.AsEnv()) - if err != nil { - if err.Error() == "unknown CNI_COMMAND: VERSION" { - return version.PluginSupports("0.1.0"), nil - } - return nil, err - } - - return e.VersionDecoder.Decode(stdoutBytes) -} diff --git a/pkg/invoke/exec_test.go b/pkg/invoke/exec_test.go deleted file mode 100644 index 33ffc2de..00000000 --- a/pkg/invoke/exec_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke_test - -import ( - "encoding/json" - "errors" - - "github.com/containernetworking/cni/pkg/invoke" - "github.com/containernetworking/cni/pkg/invoke/fakes" - "github.com/containernetworking/cni/pkg/types/current" - "github.com/containernetworking/cni/pkg/version" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Executing a plugin, unit tests", func() { - var ( - pluginExec *invoke.PluginExec - rawExec *fakes.RawExec - versionDecoder *fakes.VersionDecoder - - pluginPath string - netconf []byte - cniargs *fakes.CNIArgs - ) - - BeforeEach(func() { - rawExec = &fakes.RawExec{} - rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`) - - versionDecoder = &fakes.VersionDecoder{} - versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0") - - pluginExec = &invoke.PluginExec{ - RawExec: rawExec, - VersionDecoder: versionDecoder, - } - pluginPath = "/some/plugin/path" - netconf = []byte(`{ "some": "stdin", "cniVersion": "0.3.1" }`) - cniargs = &fakes.CNIArgs{} - cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"} - }) - - Describe("returning a result", func() { - It("unmarshals the result bytes into the Result type", func() { - r, err := pluginExec.WithResult(pluginPath, netconf, cniargs) - Expect(err).NotTo(HaveOccurred()) - - result, err := current.GetResult(r) - Expect(err).NotTo(HaveOccurred()) - Expect(len(result.IPs)).To(Equal(1)) - Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4")) - }) - - It("passes its arguments through to the rawExec", func() { - pluginExec.WithResult(pluginPath, netconf, cniargs) - Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) - Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) - Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) - }) - - Context("when the rawExec fails", func() { - BeforeEach(func() { - rawExec.ExecPluginCall.Returns.Error = errors.New("banana") - }) - It("returns the error", func() { - _, err := pluginExec.WithResult(pluginPath, netconf, cniargs) - Expect(err).To(MatchError("banana")) - }) - }) - }) - - Describe("without returning a result", func() { - It("passes its arguments through to the rawExec", func() { - pluginExec.WithoutResult(pluginPath, netconf, cniargs) - Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) - Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf)) - Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"})) - }) - - Context("when the rawExec fails", func() { - BeforeEach(func() { - rawExec.ExecPluginCall.Returns.Error = errors.New("banana") - }) - It("returns the error", func() { - err := pluginExec.WithoutResult(pluginPath, netconf, cniargs) - Expect(err).To(MatchError("banana")) - }) - }) - }) - - Describe("discovering the plugin version", func() { - BeforeEach(func() { - rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`) - }) - - It("execs the plugin with the command VERSION", func() { - pluginExec.GetVersionInfo(pluginPath) - Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath)) - Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION")) - expectedStdin, _ := json.Marshal(map[string]string{"cniVersion": version.Current()}) - Expect(rawExec.ExecPluginCall.Received.StdinData).To(MatchJSON(expectedStdin)) - }) - - It("decodes and returns the version info", func() { - versionInfo, err := pluginExec.GetVersionInfo(pluginPath) - Expect(err).NotTo(HaveOccurred()) - Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"})) - Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`)) - }) - - Context("when the rawExec fails", func() { - BeforeEach(func() { - rawExec.ExecPluginCall.Returns.Error = errors.New("banana") - }) - It("returns the error", func() { - _, err := pluginExec.GetVersionInfo(pluginPath) - Expect(err).To(MatchError("banana")) - }) - }) - - Context("when the plugin is too old to recognize the VERSION command", func() { - BeforeEach(func() { - rawExec.ExecPluginCall.Returns.Error = errors.New("unknown CNI_COMMAND: VERSION") - }) - - It("interprets the error as a 0.1.0 version", func() { - versionInfo, err := pluginExec.GetVersionInfo(pluginPath) - Expect(err).NotTo(HaveOccurred()) - Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0")) - }) - - It("sets dummy values for env vars required by very old plugins", func() { - pluginExec.GetVersionInfo(pluginPath) - - env := rawExec.ExecPluginCall.Received.Environ - Expect(env).To(ContainElement("CNI_NETNS=dummy")) - Expect(env).To(ContainElement("CNI_IFNAME=dummy")) - Expect(env).To(ContainElement("CNI_PATH=dummy")) - }) - }) - }) -}) diff --git a/pkg/invoke/fakes/cni_args.go b/pkg/invoke/fakes/cni_args.go deleted file mode 100644 index 5b1ba29e..00000000 --- a/pkg/invoke/fakes/cni_args.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fakes - -type CNIArgs struct { - AsEnvCall struct { - Returns struct { - Env []string - } - } -} - -func (a *CNIArgs) AsEnv() []string { - return a.AsEnvCall.Returns.Env -} diff --git a/pkg/invoke/fakes/raw_exec.go b/pkg/invoke/fakes/raw_exec.go deleted file mode 100644 index 5432cdf7..00000000 --- a/pkg/invoke/fakes/raw_exec.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fakes - -type RawExec struct { - ExecPluginCall struct { - Received struct { - PluginPath string - StdinData []byte - Environ []string - } - Returns struct { - ResultBytes []byte - Error error - } - } -} - -func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { - e.ExecPluginCall.Received.PluginPath = pluginPath - e.ExecPluginCall.Received.StdinData = stdinData - e.ExecPluginCall.Received.Environ = environ - return e.ExecPluginCall.Returns.ResultBytes, e.ExecPluginCall.Returns.Error -} diff --git a/pkg/invoke/fakes/version_decoder.go b/pkg/invoke/fakes/version_decoder.go deleted file mode 100644 index 72d29733..00000000 --- a/pkg/invoke/fakes/version_decoder.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fakes - -import "github.com/containernetworking/cni/pkg/version" - -type VersionDecoder struct { - DecodeCall struct { - Received struct { - JSONBytes []byte - } - Returns struct { - PluginInfo version.PluginInfo - Error error - } - } -} - -func (e *VersionDecoder) Decode(jsonData []byte) (version.PluginInfo, error) { - e.DecodeCall.Received.JSONBytes = jsonData - return e.DecodeCall.Returns.PluginInfo, e.DecodeCall.Returns.Error -} diff --git a/pkg/invoke/find.go b/pkg/invoke/find.go deleted file mode 100644 index e815404c..00000000 --- a/pkg/invoke/find.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -import ( - "fmt" - "os" - "path/filepath" -) - -// FindInPath returns the full path of the plugin by searching in the provided path -func FindInPath(plugin string, paths []string) (string, error) { - if plugin == "" { - return "", fmt.Errorf("no plugin name provided") - } - - if len(paths) == 0 { - return "", fmt.Errorf("no paths provided") - } - - for _, path := range paths { - for _, fe := range ExecutableFileExtensions { - fullpath := filepath.Join(path, plugin) + fe - if fi, err := os.Stat(fullpath); err == nil && fi.Mode().IsRegular() { - return fullpath, nil - } - } - } - - return "", fmt.Errorf("failed to find plugin %q in path %s", plugin, paths) -} diff --git a/pkg/invoke/find_test.go b/pkg/invoke/find_test.go deleted file mode 100644 index 58543131..00000000 --- a/pkg/invoke/find_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke_test - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/containernetworking/cni/pkg/invoke" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("FindInPath", func() { - var ( - multiplePaths []string - pluginName string - plugin2NameWithExt string - plugin2NameWithoutExt string - pluginDir string - anotherTempDir string - ) - - BeforeEach(func() { - tempDir, err := ioutil.TempDir("", "cni-find") - Expect(err).NotTo(HaveOccurred()) - - plugin, err := ioutil.TempFile(tempDir, "a-cni-plugin") - Expect(err).NotTo(HaveOccurred()) - - plugin2Name := "a-plugin-with-extension" + invoke.ExecutableFileExtensions[0] - plugin2, err := os.Create(filepath.Join(tempDir, plugin2Name)) - Expect(err).NotTo(HaveOccurred()) - - anotherTempDir, err = ioutil.TempDir("", "nothing-here") - Expect(err).NotTo(HaveOccurred()) - - multiplePaths = []string{anotherTempDir, tempDir} - pluginDir, pluginName = filepath.Split(plugin.Name()) - _, plugin2NameWithExt = filepath.Split(plugin2.Name()) - plugin2NameWithoutExt = strings.Split(plugin2NameWithExt, ".")[0] - }) - - AfterEach(func() { - os.RemoveAll(pluginDir) - os.RemoveAll(anotherTempDir) - }) - - Context("when multiple paths are provided", func() { - It("returns only the path to the plugin", func() { - pluginPath, err := invoke.FindInPath(pluginName, multiplePaths) - Expect(err).NotTo(HaveOccurred()) - Expect(pluginPath).To(Equal(filepath.Join(pluginDir, pluginName))) - }) - }) - - Context("when a plugin name without its file name extension is provided", func() { - It("returns the path to the plugin, including its extension", func() { - pluginPath, err := invoke.FindInPath(plugin2NameWithoutExt, multiplePaths) - Expect(err).NotTo(HaveOccurred()) - Expect(pluginPath).To(Equal(filepath.Join(pluginDir, plugin2NameWithExt))) - }) - }) - - Context("when an error occurs", func() { - Context("when no paths are provided", func() { - It("returns an error noting no paths were provided", func() { - _, err := invoke.FindInPath(pluginName, []string{}) - Expect(err).To(MatchError("no paths provided")) - }) - }) - - Context("when no plugin is provided", func() { - It("returns an error noting the plugin name wasn't found", func() { - _, err := invoke.FindInPath("", multiplePaths) - Expect(err).To(MatchError("no plugin name provided")) - }) - }) - - Context("when the plugin cannot be found", func() { - It("returns an error noting the path", func() { - pathsWithNothing := []string{anotherTempDir} - _, err := invoke.FindInPath(pluginName, pathsWithNothing) - Expect(err).To(MatchError(fmt.Sprintf("failed to find plugin %q in path %s", pluginName, pathsWithNothing))) - }) - }) - }) -}) diff --git a/pkg/invoke/get_version_integration_test.go b/pkg/invoke/get_version_integration_test.go deleted file mode 100644 index 7e58a9be..00000000 --- a/pkg/invoke/get_version_integration_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - "github.com/containernetworking/cni/pkg/invoke" - "github.com/containernetworking/cni/pkg/version" - "github.com/containernetworking/cni/pkg/version/testhelpers" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("GetVersion, integration tests", func() { - var ( - pluginDir string - pluginPath string - ) - - BeforeEach(func() { - pluginDir, err := ioutil.TempDir("", "plugins") - Expect(err).NotTo(HaveOccurred()) - pluginPath = filepath.Join(pluginDir, "test-plugin") - }) - - AfterEach(func() { - Expect(os.RemoveAll(pluginDir)).To(Succeed()) - }) - - DescribeTable("correctly reporting plugin versions", - func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) { - Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed()) - versionInfo, err := invoke.GetVersionInfo(pluginPath) - Expect(err).NotTo(HaveOccurred()) - - Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions())) - }, - - Entry("historical: before VERSION was introduced", - git_ref_v010, plugin_source_no_custom_versions, - version.PluginSupports("0.1.0"), - ), - - Entry("historical: when VERSION was introduced but plugins couldn't customize it", - git_ref_v020_no_custom_versions, plugin_source_no_custom_versions, - version.PluginSupports("0.1.0", "0.2.0"), - ), - - Entry("historical: when plugins started reporting their own version list", - git_ref_v020_custom_versions, plugin_source_v020_custom_versions, - version.PluginSupports("0.2.0", "0.999.0"), - ), - - // this entry tracks the current behavior. Before you change it, ensure - // that its previous behavior is captured in the most recent "historical" entry - Entry("current", - "HEAD", plugin_source_v020_custom_versions, - version.PluginSupports("0.2.0", "0.999.0"), - ), - ) -}) - -// a 0.2.0 plugin that can report its own versions -const plugin_source_v020_custom_versions = `package main - -import ( - "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/version" - "fmt" -) - -func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil } - -func main() { skel.PluginMain(c, c, version.PluginSupports("0.2.0", "0.999.0")) } -` -const git_ref_v020_custom_versions = "bf31ed15" - -// a minimal 0.1.0 / 0.2.0 plugin that cannot report it's own version support -const plugin_source_no_custom_versions = `package main - -import "github.com/containernetworking/cni/pkg/skel" -import "fmt" - -func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil } - -func main() { skel.PluginMain(c, c) } -` - -const git_ref_v010 = "2c482f4" -const git_ref_v020_no_custom_versions = "349d66d" diff --git a/pkg/invoke/invoke_suite_test.go b/pkg/invoke/invoke_suite_test.go deleted file mode 100644 index 7285878a..00000000 --- a/pkg/invoke/invoke_suite_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gexec" - - "testing" -) - -func TestInvoke(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Invoke Suite") -} - -const packagePath = "github.com/containernetworking/cni/plugins/test/noop" - -var pathToPlugin string - -var _ = SynchronizedBeforeSuite(func() []byte { - var err error - pathToPlugin, err = gexec.Build(packagePath) - Expect(err).NotTo(HaveOccurred()) - return []byte(pathToPlugin) -}, func(crossNodeData []byte) { - pathToPlugin = string(crossNodeData) -}) - -var _ = SynchronizedAfterSuite(func() {}, func() { - gexec.CleanupBuildArtifacts() -}) diff --git a/pkg/invoke/os_unix.go b/pkg/invoke/os_unix.go deleted file mode 100644 index bab5737a..00000000 --- a/pkg/invoke/os_unix.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// +build darwin dragonfly freebsd linux netbsd opensbd solaris - -package invoke - -// Valid file extensions for plugin executables. -var ExecutableFileExtensions = []string{""} diff --git a/pkg/invoke/os_windows.go b/pkg/invoke/os_windows.go deleted file mode 100644 index 7665125b..00000000 --- a/pkg/invoke/os_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -// Valid file extensions for plugin executables. -var ExecutableFileExtensions = []string{".exe", ""} diff --git a/pkg/invoke/raw_exec.go b/pkg/invoke/raw_exec.go deleted file mode 100644 index d1bd860d..00000000 --- a/pkg/invoke/raw_exec.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os/exec" - - "github.com/containernetworking/cni/pkg/types" -) - -type RawExec struct { - Stderr io.Writer -} - -func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) { - stdout := &bytes.Buffer{} - - c := exec.Cmd{ - Env: environ, - Path: pluginPath, - Args: []string{pluginPath}, - Stdin: bytes.NewBuffer(stdinData), - Stdout: stdout, - Stderr: e.Stderr, - } - if err := c.Run(); err != nil { - return nil, pluginErr(err, stdout.Bytes()) - } - - return stdout.Bytes(), nil -} - -func pluginErr(err error, output []byte) error { - if _, ok := err.(*exec.ExitError); ok { - emsg := types.Error{} - if perr := json.Unmarshal(output, &emsg); perr != nil { - return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr) - } - details := "" - if emsg.Details != "" { - details = fmt.Sprintf("; %v", emsg.Details) - } - return fmt.Errorf("%v%v", emsg.Msg, details) - } - - return err -} diff --git a/pkg/invoke/raw_exec_test.go b/pkg/invoke/raw_exec_test.go deleted file mode 100644 index 5d759f24..00000000 --- a/pkg/invoke/raw_exec_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package invoke_test - -import ( - "bytes" - "io/ioutil" - "os" - - "github.com/containernetworking/cni/pkg/invoke" - - noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("RawExec", func() { - var ( - debugFileName string - debug *noop_debug.Debug - environ []string - stdin []byte - execer *invoke.RawExec - ) - - const reportResult = `{ "some": "result" }` - - BeforeEach(func() { - debugFile, err := ioutil.TempFile("", "cni_debug") - Expect(err).NotTo(HaveOccurred()) - Expect(debugFile.Close()).To(Succeed()) - debugFileName = debugFile.Name() - - debug = &noop_debug.Debug{ - ReportResult: reportResult, - ReportStderr: "some stderr message", - } - Expect(debug.WriteDebug(debugFileName)).To(Succeed()) - - environ = []string{ - "CNI_COMMAND=ADD", - "CNI_CONTAINERID=some-container-id", - "CNI_ARGS=DEBUG=" + debugFileName, - "CNI_NETNS=/some/netns/path", - "CNI_PATH=/some/bin/path", - "CNI_IFNAME=some-eth0", - } - stdin = []byte(`{"some":"stdin-json", "cniVersion": "0.3.1"}`) - execer = &invoke.RawExec{} - }) - - AfterEach(func() { - Expect(os.Remove(debugFileName)).To(Succeed()) - }) - - It("runs the plugin with the given stdin and environment", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).NotTo(HaveOccurred()) - - debug, err := noop_debug.ReadDebug(debugFileName) - Expect(err).NotTo(HaveOccurred()) - Expect(debug.Command).To(Equal("ADD")) - Expect(debug.CmdArgs.StdinData).To(Equal(stdin)) - Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path")) - }) - - It("returns the resulting stdout as bytes", func() { - resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).NotTo(HaveOccurred()) - - Expect(resultBytes).To(BeEquivalentTo(reportResult)) - }) - - Context("when the Stderr writer is set", func() { - var stderrBuffer *bytes.Buffer - - BeforeEach(func() { - stderrBuffer = &bytes.Buffer{} - execer.Stderr = stderrBuffer - }) - - It("forwards any stderr bytes to the Stderr writer", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).NotTo(HaveOccurred()) - - Expect(stderrBuffer.String()).To(Equal("some stderr message")) - }) - }) - - Context("when the plugin errors", func() { - BeforeEach(func() { - debug.ReportError = "banana" - Expect(debug.WriteDebug(debugFileName)).To(Succeed()) - }) - - It("wraps and returns the error", func() { - _, err := execer.ExecPlugin(pathToPlugin, stdin, environ) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError("banana")) - }) - }) - - Context("when the system is unable to execute the plugin", func() { - It("returns the error", func() { - _, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path"))) - }) - }) -}) diff --git a/pkg/ip/link.go b/pkg/ip/link.go index a9842627..fb8d3d50 100644 --- a/pkg/ip/link.go +++ b/pkg/ip/link.go @@ -21,8 +21,8 @@ import ( "net" "os" - "github.com/containernetworking/cni/pkg/ns" - "github.com/containernetworking/cni/pkg/utils/hwaddr" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/utils/hwaddr" "github.com/vishvananda/netlink" ) diff --git a/pkg/ip/link_test.go b/pkg/ip/link_test.go index 23182a54..a20a1582 100644 --- a/pkg/ip/link_test.go +++ b/pkg/ip/link_test.go @@ -23,8 +23,8 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" "github.com/vishvananda/netlink/nl" diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index b76780f0..54f80c0e 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -20,9 +20,9 @@ import ( "os" "github.com/containernetworking/cni/pkg/invoke" - "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ip" "github.com/vishvananda/netlink" ) diff --git a/pkg/ipam/ipam_test.go b/pkg/ipam/ipam_test.go index 2d27825d..844867f2 100644 --- a/pkg/ipam/ipam_test.go +++ b/pkg/ipam/ipam_test.go @@ -18,9 +18,9 @@ import ( "net" "syscall" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" diff --git a/pkg/ns/ns_test.go b/pkg/ns/ns_test.go index 44ed2728..e47517da 100644 --- a/pkg/ns/ns_test.go +++ b/pkg/ns/ns_test.go @@ -21,7 +21,7 @@ import ( "os" "path/filepath" - "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/plugins/pkg/ns" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "golang.org/x/sys/unix" diff --git a/pkg/skel/skel.go b/pkg/skel/skel.go deleted file mode 100644 index 8644c25e..00000000 --- a/pkg/skel/skel.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2014-2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package skel provides skeleton code for a CNI plugin. -// In particular, it implements argument parsing and validation. -package skel - -import ( - "fmt" - "io" - "io/ioutil" - "log" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/version" -) - -// CmdArgs captures all the arguments passed in to the plugin -// via both env vars and stdin -type CmdArgs struct { - ContainerID string - Netns string - IfName string - Args string - Path string - StdinData []byte -} - -type dispatcher struct { - Getenv func(string) string - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer - - ConfVersionDecoder version.ConfigDecoder - VersionReconciler version.Reconciler -} - -type reqForCmdEntry map[string]bool - -func (t *dispatcher) getCmdArgsFromEnv() (string, *CmdArgs, error) { - var cmd, contID, netns, ifName, args, path string - - vars := []struct { - name string - val *string - reqForCmd reqForCmdEntry - }{ - { - "CNI_COMMAND", - &cmd, - reqForCmdEntry{ - "ADD": true, - "DEL": true, - }, - }, - { - "CNI_CONTAINERID", - &contID, - reqForCmdEntry{ - "ADD": false, - "DEL": false, - }, - }, - { - "CNI_NETNS", - &netns, - reqForCmdEntry{ - "ADD": true, - "DEL": false, - }, - }, - { - "CNI_IFNAME", - &ifName, - reqForCmdEntry{ - "ADD": true, - "DEL": true, - }, - }, - { - "CNI_ARGS", - &args, - reqForCmdEntry{ - "ADD": false, - "DEL": false, - }, - }, - { - "CNI_PATH", - &path, - reqForCmdEntry{ - "ADD": true, - "DEL": true, - }, - }, - } - - argsMissing := false - for _, v := range vars { - *v.val = t.Getenv(v.name) - if *v.val == "" { - if v.reqForCmd[cmd] || v.name == "CNI_COMMAND" { - fmt.Fprintf(t.Stderr, "%v env variable missing\n", v.name) - argsMissing = true - } - } - } - - if argsMissing { - return "", nil, fmt.Errorf("required env variables missing") - } - - stdinData, err := ioutil.ReadAll(t.Stdin) - if err != nil { - return "", nil, fmt.Errorf("error reading from stdin: %v", err) - } - - cmdArgs := &CmdArgs{ - ContainerID: contID, - Netns: netns, - IfName: ifName, - Args: args, - Path: path, - StdinData: stdinData, - } - return cmd, cmdArgs, nil -} - -func createTypedError(f string, args ...interface{}) *types.Error { - return &types.Error{ - Code: 100, - Msg: fmt.Sprintf(f, args...), - } -} - -func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error { - configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData) - if err != nil { - return err - } - verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo) - if verErr != nil { - return &types.Error{ - Code: types.ErrIncompatibleCNIVersion, - Msg: "incompatible CNI versions", - Details: verErr.Details(), - } - } - return toCall(cmdArgs) -} - -func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { - cmd, cmdArgs, err := t.getCmdArgsFromEnv() - if err != nil { - return createTypedError(err.Error()) - } - - switch cmd { - case "ADD": - err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd) - case "DEL": - err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel) - case "VERSION": - err = versionInfo.Encode(t.Stdout) - default: - return createTypedError("unknown CNI_COMMAND: %v", cmd) - } - - if err != nil { - if e, ok := err.(*types.Error); ok { - // don't wrap Error in Error - return e - } - return createTypedError(err.Error()) - } - return nil -} - -// PluginMainWithError is the core "main" for a plugin. It accepts -// callback functions for add and del CNI commands and returns an error. -// -// The caller must also specify what CNI spec versions the plugin supports. -// -// It is the responsibility of the caller to check for non-nil error return. -// -// For a plugin to comply with the CNI spec, it must print any error to stdout -// as JSON and then exit with nonzero status code. -// -// To let this package automatically handle errors and call os.Exit(1) for you, -// use PluginMain() instead. -func PluginMainWithError(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error { - return (&dispatcher{ - Getenv: os.Getenv, - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - }).pluginMain(cmdAdd, cmdDel, versionInfo) -} - -// PluginMain is the core "main" for a plugin which includes automatic error handling. -// -// The caller must also specify what CNI spec versions the plugin supports. -// -// When an error occurs in either cmdAdd or cmdDel, PluginMain will print the error -// as JSON to stdout and call os.Exit(1). -// -// To have more control over error handling, use PluginMainWithError() instead. -func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) { - if e := PluginMainWithError(cmdAdd, cmdDel, versionInfo); e != nil { - if err := e.Print(); err != nil { - log.Print("Error writing error JSON to stdout: ", err) - } - os.Exit(1) - } -} diff --git a/pkg/skel/skel_suite_test.go b/pkg/skel/skel_suite_test.go deleted file mode 100644 index df5a8e77..00000000 --- a/pkg/skel/skel_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package skel - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestSkel(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Skel Suite") -} diff --git a/pkg/skel/skel_test.go b/pkg/skel/skel_test.go deleted file mode 100644 index ad293084..00000000 --- a/pkg/skel/skel_test.go +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package skel - -import ( - "bytes" - "errors" - "strings" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/version" - - "github.com/containernetworking/cni/pkg/testutils" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -type fakeCmd struct { - CallCount int - Returns struct { - Error error - } - Received struct { - CmdArgs *CmdArgs - } -} - -func (c *fakeCmd) Func(args *CmdArgs) error { - c.CallCount++ - c.Received.CmdArgs = args - return c.Returns.Error -} - -var _ = Describe("dispatching to the correct callback", func() { - var ( - environment map[string]string - stdinData string - stdout, stderr *bytes.Buffer - cmdAdd, cmdDel *fakeCmd - dispatch *dispatcher - expectedCmdArgs *CmdArgs - versionInfo version.PluginInfo - ) - - BeforeEach(func() { - environment = map[string]string{ - "CNI_COMMAND": "ADD", - "CNI_CONTAINERID": "some-container-id", - "CNI_NETNS": "/some/netns/path", - "CNI_IFNAME": "eth0", - "CNI_ARGS": "some;extra;args", - "CNI_PATH": "/some/cni/path", - } - - stdinData = `{ "some": "config", "cniVersion": "9.8.7" }` - stdout = &bytes.Buffer{} - stderr = &bytes.Buffer{} - versionInfo = version.PluginSupports("9.8.7") - dispatch = &dispatcher{ - Getenv: func(key string) string { return environment[key] }, - Stdin: strings.NewReader(stdinData), - Stdout: stdout, - Stderr: stderr, - } - cmdAdd = &fakeCmd{} - cmdDel = &fakeCmd{} - expectedCmdArgs = &CmdArgs{ - ContainerID: "some-container-id", - Netns: "/some/netns/path", - IfName: "eth0", - Args: "some;extra;args", - Path: "/some/cni/path", - StdinData: []byte(stdinData), - } - }) - - var envVarChecker = func(envVar string, isRequired bool) { - delete(environment, envVar) - - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - if isRequired { - Expect(err).To(Equal(&types.Error{ - Code: 100, - Msg: "required env variables missing", - })) - Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n")) - } else { - Expect(err).NotTo(HaveOccurred()) - } - } - - Context("when the CNI_COMMAND is ADD", func() { - It("extracts env vars and stdin data and calls cmdAdd", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(cmdAdd.CallCount).To(Equal(1)) - Expect(cmdDel.CallCount).To(Equal(0)) - Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) - }) - - It("does not call cmdDel", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(cmdDel.CallCount).To(Equal(0)) - }) - - DescribeTable("required / optional env vars", envVarChecker, - Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINERID", false), - Entry("net ns", "CNI_NETNS", true), - Entry("if name", "CNI_IFNAME", true), - Entry("args", "CNI_ARGS", false), - Entry("path", "CNI_PATH", true), - ) - - Context("when multiple required env vars are missing", func() { - BeforeEach(func() { - delete(environment, "CNI_NETNS") - delete(environment, "CNI_IFNAME") - delete(environment, "CNI_PATH") - }) - - It("reports that all of them are missing, not just the first", func() { - Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)).NotTo(Succeed()) - log := stderr.String() - Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n")) - Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n")) - Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n")) - - }) - }) - - Context("when the stdin data is missing the required cniVersion config", func() { - BeforeEach(func() { - dispatch.Stdin = strings.NewReader(`{ "some": "config" }`) - }) - - Context("when the plugin supports version 0.1.0", func() { - BeforeEach(func() { - versionInfo = version.PluginSupports("0.1.0") - expectedCmdArgs.StdinData = []byte(`{ "some": "config" }`) - }) - - It("infers the config is 0.1.0 and calls the cmdAdd callback", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - Expect(err).NotTo(HaveOccurred()) - - Expect(cmdAdd.CallCount).To(Equal(1)) - Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) - }) - }) - - Context("when the plugin does not support 0.1.0", func() { - BeforeEach(func() { - versionInfo = version.PluginSupports("4.3.2") - }) - - It("immediately returns a useful error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes - Expect(err.Msg).To(Equal("incompatible CNI versions")) - Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`)) - }) - - It("does not call either callback", func() { - dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - Expect(cmdAdd.CallCount).To(Equal(0)) - Expect(cmdDel.CallCount).To(Equal(0)) - }) - }) - }) - }) - - Context("when the CNI_COMMAND is DEL", func() { - BeforeEach(func() { - environment["CNI_COMMAND"] = "DEL" - }) - - It("calls cmdDel with the env vars and stdin data", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(cmdDel.CallCount).To(Equal(1)) - Expect(cmdDel.Received.CmdArgs).To(Equal(expectedCmdArgs)) - }) - - It("does not call cmdAdd", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(cmdAdd.CallCount).To(Equal(0)) - }) - - DescribeTable("required / optional env vars", envVarChecker, - Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINERID", false), - Entry("net ns", "CNI_NETNS", false), - Entry("if name", "CNI_IFNAME", true), - Entry("args", "CNI_ARGS", false), - Entry("path", "CNI_PATH", true), - ) - }) - - Context("when the CNI_COMMAND is VERSION", func() { - BeforeEach(func() { - environment["CNI_COMMAND"] = "VERSION" - }) - - It("prints the version to stdout", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.3.1", - "supportedVersions": ["9.8.7"] - }`)) - }) - - It("does not call cmdAdd or cmdDel", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(cmdAdd.CallCount).To(Equal(0)) - Expect(cmdDel.CallCount).To(Equal(0)) - }) - - DescribeTable("VERSION does not need the usual env vars", envVarChecker, - Entry("command", "CNI_COMMAND", true), - Entry("container id", "CNI_CONTAINERID", false), - Entry("net ns", "CNI_NETNS", false), - Entry("if name", "CNI_IFNAME", false), - Entry("args", "CNI_ARGS", false), - Entry("path", "CNI_PATH", false), - ) - - Context("when the stdin is empty", func() { - BeforeEach(func() { - dispatch.Stdin = strings.NewReader("") - }) - - It("succeeds without error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).NotTo(HaveOccurred()) - Expect(stdout).To(MatchJSON(`{ - "cniVersion": "0.3.1", - "supportedVersions": ["9.8.7"] - }`)) - }) - }) - }) - - Context("when the CNI_COMMAND is unrecognized", func() { - BeforeEach(func() { - environment["CNI_COMMAND"] = "NOPE" - }) - - It("does not call any cmd callback", func() { - dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(cmdAdd.CallCount).To(Equal(0)) - Expect(cmdDel.CallCount).To(Equal(0)) - }) - - It("returns an error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).To(Equal(&types.Error{ - Code: 100, - Msg: "unknown CNI_COMMAND: NOPE", - })) - }) - }) - - Context("when stdin cannot be read", func() { - BeforeEach(func() { - dispatch.Stdin = &testutils.BadReader{} - }) - - It("does not call any cmd callback", func() { - dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(cmdAdd.CallCount).To(Equal(0)) - Expect(cmdDel.CallCount).To(Equal(0)) - }) - - It("wraps and returns the error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).To(Equal(&types.Error{ - Code: 100, - Msg: "error reading from stdin: banana", - })) - }) - }) - - Context("when the callback returns an error", func() { - Context("when it is a typed Error", func() { - BeforeEach(func() { - cmdAdd.Returns.Error = &types.Error{ - Code: 1234, - Msg: "insufficient something", - } - }) - - It("returns the error as-is", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).To(Equal(&types.Error{ - Code: 1234, - Msg: "insufficient something", - })) - }) - }) - - Context("when it is an unknown error", func() { - BeforeEach(func() { - cmdAdd.Returns.Error = errors.New("potato") - }) - - It("wraps and returns the error", func() { - err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo) - - Expect(err).To(Equal(&types.Error{ - Code: 100, - Msg: "potato", - })) - }) - }) - }) -}) diff --git a/pkg/types/020/types.go b/pkg/types/020/types.go deleted file mode 100644 index 2833aba7..00000000 --- a/pkg/types/020/types.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types020 - -import ( - "encoding/json" - "fmt" - "net" - "os" - - "github.com/containernetworking/cni/pkg/types" -) - -const ImplementedSpecVersion string = "0.2.0" - -var SupportedVersions = []string{"", "0.1.0", ImplementedSpecVersion} - -// Compatibility types for CNI version 0.1.0 and 0.2.0 - -func NewResult(data []byte) (types.Result, error) { - result := &Result{} - if err := json.Unmarshal(data, result); err != nil { - return nil, err - } - return result, nil -} - -func GetResult(r types.Result) (*Result, error) { - // We expect version 0.1.0/0.2.0 results - result020, err := r.GetAsVersion(ImplementedSpecVersion) - if err != nil { - return nil, err - } - result, ok := result020.(*Result) - if !ok { - return nil, fmt.Errorf("failed to convert result") - } - return result, nil -} - -// Result is what gets returned from the plugin (via stdout) to the caller -type Result struct { - CNIVersion string `json:"cniVersion,omitempty"` - IP4 *IPConfig `json:"ip4,omitempty"` - IP6 *IPConfig `json:"ip6,omitempty"` - DNS types.DNS `json:"dns,omitempty"` -} - -func (r *Result) Version() string { - return ImplementedSpecVersion -} - -func (r *Result) GetAsVersion(version string) (types.Result, error) { - for _, supportedVersion := range SupportedVersions { - if version == supportedVersion { - r.CNIVersion = version - return r, nil - } - } - return nil, fmt.Errorf("cannot convert version %q to %s", SupportedVersions, version) -} - -func (r *Result) Print() error { - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err - } - _, err = os.Stdout.Write(data) - return err -} - -// String returns a formatted string in the form of "[IP4: $1,][ IP6: $2,] DNS: $3" where -// $1 represents the receiver's IPv4, $2 represents the receiver's IPv6 and $3 the -// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. -func (r *Result) String() string { - var str string - if r.IP4 != nil { - str = fmt.Sprintf("IP4:%+v, ", *r.IP4) - } - if r.IP6 != nil { - str += fmt.Sprintf("IP6:%+v, ", *r.IP6) - } - return fmt.Sprintf("%sDNS:%+v", str, r.DNS) -} - -// IPConfig contains values necessary to configure an interface -type IPConfig struct { - IP net.IPNet - Gateway net.IP - Routes []types.Route -} - -// net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom IPNet type - -// JSON (un)marshallable types -type ipConfig struct { - IP types.IPNet `json:"ip"` - Gateway net.IP `json:"gateway,omitempty"` - Routes []types.Route `json:"routes,omitempty"` -} - -func (c *IPConfig) MarshalJSON() ([]byte, error) { - ipc := ipConfig{ - IP: types.IPNet(c.IP), - Gateway: c.Gateway, - Routes: c.Routes, - } - - return json.Marshal(ipc) -} - -func (c *IPConfig) UnmarshalJSON(data []byte) error { - ipc := ipConfig{} - if err := json.Unmarshal(data, &ipc); err != nil { - return err - } - - c.IP = net.IPNet(ipc.IP) - c.Gateway = ipc.Gateway - c.Routes = ipc.Routes - return nil -} diff --git a/pkg/types/020/types_suite_test.go b/pkg/types/020/types_suite_test.go deleted file mode 100644 index 095d73e2..00000000 --- a/pkg/types/020/types_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types020_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTypes010(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "0.1.0/0.2.0 Types Suite") -} diff --git a/pkg/types/020/types_test.go b/pkg/types/020/types_test.go deleted file mode 100644 index 4f08ca49..00000000 --- a/pkg/types/020/types_test.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types020_test - -import ( - "io/ioutil" - "net" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Ensures compatibility with the 0.1.0/0.2.0 spec", func() { - It("correctly encodes a 0.1.0/0.2.0 Result", func() { - ipv4, err := types.ParseCIDR("1.2.3.30/24") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv4).NotTo(BeNil()) - - routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") - Expect(err).NotTo(HaveOccurred()) - Expect(routev4).NotTo(BeNil()) - Expect(routegwv4).NotTo(BeNil()) - - ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv6).NotTo(BeNil()) - - routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") - Expect(err).NotTo(HaveOccurred()) - Expect(routev6).NotTo(BeNil()) - Expect(routegwv6).NotTo(BeNil()) - - // Set every field of the struct to ensure source compatibility - res := types020.Result{ - CNIVersion: types020.ImplementedSpecVersion, - IP4: &types020.IPConfig{ - IP: *ipv4, - Gateway: net.ParseIP("1.2.3.1"), - Routes: []types.Route{ - {Dst: *routev4, GW: routegwv4}, - }, - }, - IP6: &types020.IPConfig{ - IP: *ipv6, - Gateway: net.ParseIP("abcd:1234:ffff::1"), - Routes: []types.Route{ - {Dst: *routev6, GW: routegwv6}, - }, - }, - DNS: types.DNS{ - Nameservers: []string{"1.2.3.4", "1::cafe"}, - Domain: "acompany.com", - Search: []string{"somedomain.com", "otherdomain.net"}, - Options: []string{"foo", "bar"}, - }, - } - - Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) - - // Redirect stdout to capture JSON result - oldStdout := os.Stdout - r, w, err := os.Pipe() - Expect(err).NotTo(HaveOccurred()) - - os.Stdout = w - err = res.Print() - w.Close() - Expect(err).NotTo(HaveOccurred()) - - // parse the result - out, err := ioutil.ReadAll(r) - os.Stdout = oldStdout - Expect(err).NotTo(HaveOccurred()) - - Expect(string(out)).To(Equal(`{ - "cniVersion": "0.2.0", - "ip4": { - "ip": "1.2.3.30/24", - "gateway": "1.2.3.1", - "routes": [ - { - "dst": "15.5.6.0/24", - "gw": "15.5.6.8" - } - ] - }, - "ip6": { - "ip": "abcd:1234:ffff::cdde/64", - "gateway": "abcd:1234:ffff::1", - "routes": [ - { - "dst": "1111:dddd::/80", - "gw": "1111:dddd::aaaa" - } - ] - }, - "dns": { - "nameservers": [ - "1.2.3.4", - "1::cafe" - ], - "domain": "acompany.com", - "search": [ - "somedomain.com", - "otherdomain.net" - ], - "options": [ - "foo", - "bar" - ] - } -}`)) - }) -}) diff --git a/pkg/types/args.go b/pkg/types/args.go deleted file mode 100644 index 66dcf9ea..00000000 --- a/pkg/types/args.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "encoding" - "fmt" - "reflect" - "strings" -) - -// UnmarshallableBool typedef for builtin bool -// because builtin type's methods can't be declared -type UnmarshallableBool bool - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// Returns boolean true if the string is "1" or "[Tt]rue" -// Returns boolean false if the string is "0" or "[Ff]alse" -func (b *UnmarshallableBool) UnmarshalText(data []byte) error { - s := strings.ToLower(string(data)) - switch s { - case "1", "true": - *b = true - case "0", "false": - *b = false - default: - return fmt.Errorf("Boolean unmarshal error: invalid input %s", s) - } - return nil -} - -// UnmarshallableString typedef for builtin string -type UnmarshallableString string - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// Returns the string -func (s *UnmarshallableString) UnmarshalText(data []byte) error { - *s = UnmarshallableString(data) - return nil -} - -// CommonArgs contains the IgnoreUnknown argument -// and must be embedded by all Arg structs -type CommonArgs struct { - IgnoreUnknown UnmarshallableBool `json:"ignoreunknown,omitempty"` -} - -// GetKeyField is a helper function to receive Values -// Values that represent a pointer to a struct -func GetKeyField(keyString string, v reflect.Value) reflect.Value { - return v.Elem().FieldByName(keyString) -} - -// LoadArgs parses args from a string in the form "K=V;K2=V2;..." -func LoadArgs(args string, container interface{}) error { - if args == "" { - return nil - } - - containerValue := reflect.ValueOf(container) - - pairs := strings.Split(args, ";") - unknownArgs := []string{} - for _, pair := range pairs { - kv := strings.Split(pair, "=") - if len(kv) != 2 { - return fmt.Errorf("ARGS: invalid pair %q", pair) - } - keyString := kv[0] - valueString := kv[1] - keyField := GetKeyField(keyString, containerValue) - if !keyField.IsValid() { - unknownArgs = append(unknownArgs, pair) - continue - } - - u := keyField.Addr().Interface().(encoding.TextUnmarshaler) - err := u.UnmarshalText([]byte(valueString)) - if err != nil { - return fmt.Errorf("ARGS: error parsing value of pair %q: %v)", pair, err) - } - } - - isIgnoreUnknown := GetKeyField("IgnoreUnknown", containerValue).Bool() - if len(unknownArgs) > 0 && !isIgnoreUnknown { - return fmt.Errorf("ARGS: unknown args %q", unknownArgs) - } - return nil -} diff --git a/pkg/types/args_test.go b/pkg/types/args_test.go deleted file mode 100644 index 3a53d9a4..00000000 --- a/pkg/types/args_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types_test - -import ( - "reflect" - - . "github.com/containernetworking/cni/pkg/types" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("UnmarshallableBool UnmarshalText", func() { - DescribeTable("string to bool detection should succeed in all cases", - func(inputs []string, expected bool) { - for _, s := range inputs { - var ub UnmarshallableBool - err := ub.UnmarshalText([]byte(s)) - Expect(err).ToNot(HaveOccurred()) - Expect(ub).To(Equal(UnmarshallableBool(expected))) - } - }, - Entry("parse to true", []string{"True", "true", "1"}, true), - Entry("parse to false", []string{"False", "false", "0"}, false), - ) - - Context("When passed an invalid value", func() { - It("should result in an error", func() { - var ub UnmarshallableBool - err := ub.UnmarshalText([]byte("invalid")) - Expect(err).To(HaveOccurred()) - }) - }) -}) - -var _ = Describe("UnmarshallableString UnmarshalText", func() { - DescribeTable("string to string detection should succeed in all cases", - func(inputs []string, expected string) { - for _, s := range inputs { - var us UnmarshallableString - err := us.UnmarshalText([]byte(s)) - Expect(err).ToNot(HaveOccurred()) - Expect(string(us)).To(Equal(expected)) - } - }, - Entry("parse empty string", []string{""}, ""), - Entry("parse non-empty string", []string{"notempty"}, "notempty"), - ) -}) - -var _ = Describe("GetKeyField", func() { - type testcontainer struct { - Valid string `json:"valid,omitempty"` - } - var ( - container = testcontainer{Valid: "valid"} - containerInterface = func(i interface{}) interface{} { return i }(&container) - containerValue = reflect.ValueOf(containerInterface) - ) - Context("When a valid field is provided", func() { - It("should return the correct field", func() { - field := GetKeyField("Valid", containerValue) - Expect(field.String()).To(Equal("valid")) - }) - }) -}) - -var _ = Describe("LoadArgs", func() { - Context("When no arguments are passed", func() { - It("LoadArgs should succeed", func() { - err := LoadArgs("", struct{}{}) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("When unknown arguments are passed and ignored", func() { - It("LoadArgs should succeed", func() { - ca := CommonArgs{} - err := LoadArgs("IgnoreUnknown=True;Unk=nown", &ca) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("When unknown arguments are passed and not ignored", func() { - It("LoadArgs should fail", func() { - ca := CommonArgs{} - err := LoadArgs("Unk=nown", &ca) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("When unknown arguments are passed and explicitly not ignored", func() { - It("LoadArgs should fail", func() { - ca := CommonArgs{} - err := LoadArgs("IgnoreUnknown=0, Unk=nown", &ca) - Expect(err).To(HaveOccurred()) - }) - }) - - Context("When known arguments are passed", func() { - It("LoadArgs should succeed", func() { - ca := CommonArgs{} - err := LoadArgs("IgnoreUnknown=1", &ca) - Expect(err).NotTo(HaveOccurred()) - }) - }) -}) diff --git a/pkg/types/current/types.go b/pkg/types/current/types.go deleted file mode 100644 index b5715fe6..00000000 --- a/pkg/types/current/types.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package current - -import ( - "encoding/json" - "fmt" - "net" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" -) - -const ImplementedSpecVersion string = "0.3.1" - -var SupportedVersions = []string{"0.3.0", ImplementedSpecVersion} - -func NewResult(data []byte) (types.Result, error) { - result := &Result{} - if err := json.Unmarshal(data, result); err != nil { - return nil, err - } - return result, nil -} - -func GetResult(r types.Result) (*Result, error) { - resultCurrent, err := r.GetAsVersion(ImplementedSpecVersion) - if err != nil { - return nil, err - } - result, ok := resultCurrent.(*Result) - if !ok { - return nil, fmt.Errorf("failed to convert result") - } - return result, nil -} - -var resultConverters = []struct { - versions []string - convert func(types.Result) (*Result, error) -}{ - {types020.SupportedVersions, convertFrom020}, - {SupportedVersions, convertFrom030}, -} - -func convertFrom020(result types.Result) (*Result, error) { - oldResult, err := types020.GetResult(result) - if err != nil { - return nil, err - } - - newResult := &Result{ - CNIVersion: ImplementedSpecVersion, - DNS: oldResult.DNS, - Routes: []*types.Route{}, - } - - if oldResult.IP4 != nil { - newResult.IPs = append(newResult.IPs, &IPConfig{ - Version: "4", - Interface: -1, - Address: oldResult.IP4.IP, - Gateway: oldResult.IP4.Gateway, - }) - for _, route := range oldResult.IP4.Routes { - gw := route.GW - if gw == nil { - gw = oldResult.IP4.Gateway - } - newResult.Routes = append(newResult.Routes, &types.Route{ - Dst: route.Dst, - GW: gw, - }) - } - } - - if oldResult.IP6 != nil { - newResult.IPs = append(newResult.IPs, &IPConfig{ - Version: "6", - Interface: -1, - Address: oldResult.IP6.IP, - Gateway: oldResult.IP6.Gateway, - }) - for _, route := range oldResult.IP6.Routes { - gw := route.GW - if gw == nil { - gw = oldResult.IP6.Gateway - } - newResult.Routes = append(newResult.Routes, &types.Route{ - Dst: route.Dst, - GW: gw, - }) - } - } - - if len(newResult.IPs) == 0 { - return nil, fmt.Errorf("cannot convert: no valid IP addresses") - } - - return newResult, nil -} - -func convertFrom030(result types.Result) (*Result, error) { - newResult, ok := result.(*Result) - if !ok { - return nil, fmt.Errorf("failed to convert result") - } - newResult.CNIVersion = ImplementedSpecVersion - return newResult, nil -} - -func NewResultFromResult(result types.Result) (*Result, error) { - version := result.Version() - for _, converter := range resultConverters { - for _, supportedVersion := range converter.versions { - if version == supportedVersion { - return converter.convert(result) - } - } - } - return nil, fmt.Errorf("unsupported CNI result22 version %q", version) -} - -// Result is what gets returned from the plugin (via stdout) to the caller -type Result struct { - CNIVersion string `json:"cniVersion,omitempty"` - Interfaces []*Interface `json:"interfaces,omitempty"` - IPs []*IPConfig `json:"ips,omitempty"` - Routes []*types.Route `json:"routes,omitempty"` - DNS types.DNS `json:"dns,omitempty"` -} - -// Convert to the older 0.2.0 CNI spec Result type -func (r *Result) convertTo020() (*types020.Result, error) { - oldResult := &types020.Result{ - CNIVersion: types020.ImplementedSpecVersion, - DNS: r.DNS, - } - - for _, ip := range r.IPs { - // Only convert the first IP address of each version as 0.2.0 - // and earlier cannot handle multiple IP addresses - if ip.Version == "4" && oldResult.IP4 == nil { - oldResult.IP4 = &types020.IPConfig{ - IP: ip.Address, - Gateway: ip.Gateway, - } - } else if ip.Version == "6" && oldResult.IP6 == nil { - oldResult.IP6 = &types020.IPConfig{ - IP: ip.Address, - Gateway: ip.Gateway, - } - } - - if oldResult.IP4 != nil && oldResult.IP6 != nil { - break - } - } - - for _, route := range r.Routes { - is4 := route.Dst.IP.To4() != nil - if is4 && oldResult.IP4 != nil { - oldResult.IP4.Routes = append(oldResult.IP4.Routes, types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } else if !is4 && oldResult.IP6 != nil { - oldResult.IP6.Routes = append(oldResult.IP6.Routes, types.Route{ - Dst: route.Dst, - GW: route.GW, - }) - } - } - - if oldResult.IP4 == nil && oldResult.IP6 == nil { - return nil, fmt.Errorf("cannot convert: no valid IP addresses") - } - - return oldResult, nil -} - -func (r *Result) Version() string { - return ImplementedSpecVersion -} - -func (r *Result) GetAsVersion(version string) (types.Result, error) { - switch version { - case "0.3.0", ImplementedSpecVersion: - r.CNIVersion = version - return r, nil - case types020.SupportedVersions[0], types020.SupportedVersions[1], types020.SupportedVersions[2]: - return r.convertTo020() - } - return nil, fmt.Errorf("cannot convert version 0.3.x to %q", version) -} - -func (r *Result) Print() error { - data, err := json.MarshalIndent(r, "", " ") - if err != nil { - return err - } - _, err = os.Stdout.Write(data) - return err -} - -// String returns a formatted string in the form of "[Interfaces: $1,][ IP: $2,] DNS: $3" where -// $1 represents the receiver's Interfaces, $2 represents the receiver's IP addresses and $3 the -// receiver's DNS. If $1 or $2 are nil, they won't be present in the returned string. -func (r *Result) String() string { - var str string - if len(r.Interfaces) > 0 { - str += fmt.Sprintf("Interfaces:%+v, ", r.Interfaces) - } - if len(r.IPs) > 0 { - str += fmt.Sprintf("IP:%+v, ", r.IPs) - } - if len(r.Routes) > 0 { - str += fmt.Sprintf("Routes:%+v, ", r.Routes) - } - return fmt.Sprintf("%sDNS:%+v", str, r.DNS) -} - -// Convert this old version result to the current CNI version result -func (r *Result) Convert() (*Result, error) { - return r, nil -} - -// Interface contains values about the created interfaces -type Interface struct { - Name string `json:"name"` - Mac string `json:"mac,omitempty"` - Sandbox string `json:"sandbox,omitempty"` -} - -func (i *Interface) String() string { - return fmt.Sprintf("%+v", *i) -} - -// IPConfig contains values necessary to configure an IP address on an interface -type IPConfig struct { - // IP version, either "4" or "6" - Version string - // Index into Result structs Interfaces list - Interface int - Address net.IPNet - Gateway net.IP -} - -func (i *IPConfig) String() string { - return fmt.Sprintf("%+v", *i) -} - -// JSON (un)marshallable types -type ipConfig struct { - Version string `json:"version"` - Interface int `json:"interface,omitempty"` - Address types.IPNet `json:"address"` - Gateway net.IP `json:"gateway,omitempty"` -} - -func (c *IPConfig) MarshalJSON() ([]byte, error) { - ipc := ipConfig{ - Version: c.Version, - Interface: c.Interface, - Address: types.IPNet(c.Address), - Gateway: c.Gateway, - } - - return json.Marshal(ipc) -} - -func (c *IPConfig) UnmarshalJSON(data []byte) error { - ipc := ipConfig{} - if err := json.Unmarshal(data, &ipc); err != nil { - return err - } - - c.Version = ipc.Version - c.Interface = ipc.Interface - c.Address = net.IPNet(ipc.Address) - c.Gateway = ipc.Gateway - return nil -} diff --git a/pkg/types/current/types_suite_test.go b/pkg/types/current/types_suite_test.go deleted file mode 100644 index e05c1ff1..00000000 --- a/pkg/types/current/types_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package current_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTypesCurrent(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Current Types Suite") -} diff --git a/pkg/types/current/types_test.go b/pkg/types/current/types_test.go deleted file mode 100644 index afc68670..00000000 --- a/pkg/types/current/types_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package current_test - -import ( - "io/ioutil" - "net" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/current" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func testResult() *current.Result { - ipv4, err := types.ParseCIDR("1.2.3.30/24") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv4).NotTo(BeNil()) - - routegwv4, routev4, err := net.ParseCIDR("15.5.6.8/24") - Expect(err).NotTo(HaveOccurred()) - Expect(routev4).NotTo(BeNil()) - Expect(routegwv4).NotTo(BeNil()) - - ipv6, err := types.ParseCIDR("abcd:1234:ffff::cdde/64") - Expect(err).NotTo(HaveOccurred()) - Expect(ipv6).NotTo(BeNil()) - - routegwv6, routev6, err := net.ParseCIDR("1111:dddd::aaaa/80") - Expect(err).NotTo(HaveOccurred()) - Expect(routev6).NotTo(BeNil()) - Expect(routegwv6).NotTo(BeNil()) - - // Set every field of the struct to ensure source compatibility - return ¤t.Result{ - CNIVersion: "0.3.1", - Interfaces: []*current.Interface{ - { - Name: "eth0", - Mac: "00:11:22:33:44:55", - Sandbox: "/proc/3553/ns/net", - }, - }, - IPs: []*current.IPConfig{ - { - Version: "4", - Interface: 0, - Address: *ipv4, - Gateway: net.ParseIP("1.2.3.1"), - }, - { - Version: "6", - Interface: 0, - Address: *ipv6, - Gateway: net.ParseIP("abcd:1234:ffff::1"), - }, - }, - Routes: []*types.Route{ - {Dst: *routev4, GW: routegwv4}, - {Dst: *routev6, GW: routegwv6}, - }, - DNS: types.DNS{ - Nameservers: []string{"1.2.3.4", "1::cafe"}, - Domain: "acompany.com", - Search: []string{"somedomain.com", "otherdomain.net"}, - Options: []string{"foo", "bar"}, - }, - } -} - -var _ = Describe("Current types operations", func() { - It("correctly encodes a 0.3.x Result", func() { - res := testResult() - - Expect(res.String()).To(Equal("Interfaces:[{Name:eth0 Mac:00:11:22:33:44:55 Sandbox:/proc/3553/ns/net}], IP:[{Version:4 Interface:0 Address:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1} {Version:6 Interface:0 Address:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1}], Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8} {Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}], DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) - - // Redirect stdout to capture JSON result - oldStdout := os.Stdout - r, w, err := os.Pipe() - Expect(err).NotTo(HaveOccurred()) - - os.Stdout = w - err = res.Print() - w.Close() - Expect(err).NotTo(HaveOccurred()) - - // parse the result - out, err := ioutil.ReadAll(r) - os.Stdout = oldStdout - Expect(err).NotTo(HaveOccurred()) - - Expect(string(out)).To(Equal(`{ - "cniVersion": "0.3.1", - "interfaces": [ - { - "name": "eth0", - "mac": "00:11:22:33:44:55", - "sandbox": "/proc/3553/ns/net" - } - ], - "ips": [ - { - "version": "4", - "address": "1.2.3.30/24", - "gateway": "1.2.3.1" - }, - { - "version": "6", - "address": "abcd:1234:ffff::cdde/64", - "gateway": "abcd:1234:ffff::1" - } - ], - "routes": [ - { - "dst": "15.5.6.0/24", - "gw": "15.5.6.8" - }, - { - "dst": "1111:dddd::/80", - "gw": "1111:dddd::aaaa" - } - ], - "dns": { - "nameservers": [ - "1.2.3.4", - "1::cafe" - ], - "domain": "acompany.com", - "search": [ - "somedomain.com", - "otherdomain.net" - ], - "options": [ - "foo", - "bar" - ] - } -}`)) - }) - - It("correctly encodes a 0.1.0 Result", func() { - res, err := testResult().GetAsVersion("0.1.0") - Expect(err).NotTo(HaveOccurred()) - - Expect(res.String()).To(Equal("IP4:{IP:{IP:1.2.3.30 Mask:ffffff00} Gateway:1.2.3.1 Routes:[{Dst:{IP:15.5.6.0 Mask:ffffff00} GW:15.5.6.8}]}, IP6:{IP:{IP:abcd:1234:ffff::cdde Mask:ffffffffffffffff0000000000000000} Gateway:abcd:1234:ffff::1 Routes:[{Dst:{IP:1111:dddd:: Mask:ffffffffffffffffffff000000000000} GW:1111:dddd::aaaa}]}, DNS:{Nameservers:[1.2.3.4 1::cafe] Domain:acompany.com Search:[somedomain.com otherdomain.net] Options:[foo bar]}")) - - // Redirect stdout to capture JSON result - oldStdout := os.Stdout - r, w, err := os.Pipe() - Expect(err).NotTo(HaveOccurred()) - - os.Stdout = w - err = res.Print() - w.Close() - Expect(err).NotTo(HaveOccurred()) - - // parse the result - out, err := ioutil.ReadAll(r) - os.Stdout = oldStdout - Expect(err).NotTo(HaveOccurred()) - - Expect(string(out)).To(Equal(`{ - "cniVersion": "0.2.0", - "ip4": { - "ip": "1.2.3.30/24", - "gateway": "1.2.3.1", - "routes": [ - { - "dst": "15.5.6.0/24", - "gw": "15.5.6.8" - } - ] - }, - "ip6": { - "ip": "abcd:1234:ffff::cdde/64", - "gateway": "abcd:1234:ffff::1", - "routes": [ - { - "dst": "1111:dddd::/80", - "gw": "1111:dddd::aaaa" - } - ] - }, - "dns": { - "nameservers": [ - "1.2.3.4", - "1::cafe" - ], - "domain": "acompany.com", - "search": [ - "somedomain.com", - "otherdomain.net" - ], - "options": [ - "foo", - "bar" - ] - } -}`)) - }) -}) diff --git a/pkg/types/types.go b/pkg/types/types.go deleted file mode 100644 index 3263015a..00000000 --- a/pkg/types/types.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types - -import ( - "encoding/json" - "errors" - "fmt" - "net" - "os" -) - -// like net.IPNet but adds JSON marshalling and unmarshalling -type IPNet net.IPNet - -// ParseCIDR takes a string like "10.2.3.1/24" and -// return IPNet with "10.2.3.1" and /24 mask -func ParseCIDR(s string) (*net.IPNet, error) { - ip, ipn, err := net.ParseCIDR(s) - if err != nil { - return nil, err - } - - ipn.IP = ip - return ipn, nil -} - -func (n IPNet) MarshalJSON() ([]byte, error) { - return json.Marshal((*net.IPNet)(&n).String()) -} - -func (n *IPNet) UnmarshalJSON(data []byte) error { - var s string - if err := json.Unmarshal(data, &s); err != nil { - return err - } - - tmp, err := ParseCIDR(s) - if err != nil { - return err - } - - *n = IPNet(*tmp) - return nil -} - -// NetConf describes a network. -type NetConf struct { - CNIVersion string `json:"cniVersion,omitempty"` - - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - Capabilities map[string]bool `json:"capabilities,omitempty"` - IPAM struct { - Type string `json:"type,omitempty"` - } `json:"ipam,omitempty"` - DNS DNS `json:"dns"` -} - -// NetConfList describes an ordered list of networks. -type NetConfList struct { - CNIVersion string `json:"cniVersion,omitempty"` - - Name string `json:"name,omitempty"` - Plugins []*NetConf `json:"plugins,omitempty"` -} - -type ResultFactoryFunc func([]byte) (Result, error) - -// Result is an interface that provides the result of plugin execution -type Result interface { - // The highest CNI specification result verison the result supports - // without having to convert - Version() string - - // Returns the result converted into the requested CNI specification - // result version, or an error if conversion failed - GetAsVersion(version string) (Result, error) - - // Prints the result in JSON format to stdout - Print() error - - // Returns a JSON string representation of the result - String() string -} - -func PrintResult(result Result, version string) error { - newResult, err := result.GetAsVersion(version) - if err != nil { - return err - } - return newResult.Print() -} - -// DNS contains values interesting for DNS resolvers -type DNS struct { - Nameservers []string `json:"nameservers,omitempty"` - Domain string `json:"domain,omitempty"` - Search []string `json:"search,omitempty"` - Options []string `json:"options,omitempty"` -} - -type Route struct { - Dst net.IPNet - GW net.IP -} - -func (r *Route) String() string { - return fmt.Sprintf("%+v", *r) -} - -// Well known error codes -// see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes -const ( - ErrUnknown uint = iota // 0 - ErrIncompatibleCNIVersion // 1 - ErrUnsupportedField // 2 -) - -type Error struct { - Code uint `json:"code"` - Msg string `json:"msg"` - Details string `json:"details,omitempty"` -} - -func (e *Error) Error() string { - return e.Msg -} - -func (e *Error) Print() error { - return prettyPrint(e) -} - -// net.IPNet is not JSON (un)marshallable so this duality is needed -// for our custom IPNet type - -// JSON (un)marshallable types -type route struct { - Dst IPNet `json:"dst"` - GW net.IP `json:"gw,omitempty"` -} - -func (r *Route) UnmarshalJSON(data []byte) error { - rt := route{} - if err := json.Unmarshal(data, &rt); err != nil { - return err - } - - r.Dst = net.IPNet(rt.Dst) - r.GW = rt.GW - return nil -} - -func (r *Route) MarshalJSON() ([]byte, error) { - rt := route{ - Dst: IPNet(r.Dst), - GW: r.GW, - } - - return json.Marshal(rt) -} - -func prettyPrint(obj interface{}) error { - data, err := json.MarshalIndent(obj, "", " ") - if err != nil { - return err - } - _, err = os.Stdout.Write(data) - return err -} - -// NotImplementedError is used to indicate that a method is not implemented for the given platform -var NotImplementedError = errors.New("Not Implemented") diff --git a/pkg/types/types_suite_test.go b/pkg/types/types_suite_test.go deleted file mode 100644 index 2b178cee..00000000 --- a/pkg/types/types_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package types_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTypes(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Types Suite") -} diff --git a/pkg/utils/hwaddr/hwaddr_test.go b/pkg/utils/hwaddr/hwaddr_test.go index b77ccd89..ab89a58e 100644 --- a/pkg/utils/hwaddr/hwaddr_test.go +++ b/pkg/utils/hwaddr/hwaddr_test.go @@ -17,7 +17,7 @@ package hwaddr_test import ( "net" - "github.com/containernetworking/cni/pkg/utils/hwaddr" + "github.com/containernetworking/plugins/pkg/utils/hwaddr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/pkg/version/conf.go b/pkg/version/conf.go deleted file mode 100644 index 3cca58bb..00000000 --- a/pkg/version/conf.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version - -import ( - "encoding/json" - "fmt" -) - -// ConfigDecoder can decode the CNI version available in network config data -type ConfigDecoder struct{} - -func (*ConfigDecoder) Decode(jsonBytes []byte) (string, error) { - var conf struct { - CNIVersion string `json:"cniVersion"` - } - err := json.Unmarshal(jsonBytes, &conf) - if err != nil { - return "", fmt.Errorf("decoding version from network config: %s", err) - } - if conf.CNIVersion == "" { - return "0.1.0", nil - } - return conf.CNIVersion, nil -} diff --git a/pkg/version/conf_test.go b/pkg/version/conf_test.go deleted file mode 100644 index 881c57ad..00000000 --- a/pkg/version/conf_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version_test - -import ( - "github.com/containernetworking/cni/pkg/version" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Decoding the version of network config", func() { - var ( - decoder *version.ConfigDecoder - configBytes []byte - ) - - BeforeEach(func() { - decoder = &version.ConfigDecoder{} - configBytes = []byte(`{ "cniVersion": "4.3.2" }`) - }) - - Context("when the version is explict", func() { - It("returns the version", func() { - version, err := decoder.Decode(configBytes) - Expect(err).NotTo(HaveOccurred()) - - Expect(version).To(Equal("4.3.2")) - }) - }) - - Context("when the version is not present in the config", func() { - BeforeEach(func() { - configBytes = []byte(`{ "not-a-version-field": "foo" }`) - }) - - It("assumes the config is version 0.1.0", func() { - version, err := decoder.Decode(configBytes) - Expect(err).NotTo(HaveOccurred()) - - Expect(version).To(Equal("0.1.0")) - }) - }) - - Context("when the config data is malformed", func() { - BeforeEach(func() { - configBytes = []byte(`{{{`) - }) - - It("returns a useful error", func() { - _, err := decoder.Decode(configBytes) - Expect(err).To(MatchError(HavePrefix( - "decoding version from network config: invalid character", - ))) - }) - }) -}) diff --git a/pkg/version/legacy_examples/example_runtime.go b/pkg/version/legacy_examples/example_runtime.go deleted file mode 100644 index a461981f..00000000 --- a/pkg/version/legacy_examples/example_runtime.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package legacy_examples - -// An ExampleRuntime is a small program that uses libcni to invoke a network plugin. -// It should call ADD and DELETE, verifying all intermediate steps -// and data structures. -type ExampleRuntime struct { - Example - NetConfs []string // The network configuration names to pass -} - -// NetConfs are various versioned network configuration files. Examples should -// specify which version they expect -var NetConfs = map[string]string{ - "unversioned": `{ - "name": "default", - "type": "ptp", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24" - } -}`, - "0.1.0": `{ - "cniVersion": "0.1.0", - "name": "default", - "type": "ptp", - "ipam": { - "type": "host-local", - "subnet": "10.1.2.0/24" - } -}`, -} - -// V010_Runtime creates a simple ptp network configuration, then -// executes libcni against the currently-built plugins. -var V010_Runtime = ExampleRuntime{ - NetConfs: []string{"unversioned", "0.1.0"}, - Example: Example{ - Name: "example_invoker_v010", - CNIRepoGitRef: "c0d34c69", //version with ns.Do - PluginSource: `package main - -import ( - "fmt" - "io/ioutil" - "net" - "os" - - "github.com/containernetworking/cni/pkg/ns" - "github.com/containernetworking/cni/libcni" -) - -func main(){ - code := exec() - os.Exit(code) -} - -func exec() int { - confBytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - fmt.Printf("could not read netconfig from stdin: %+v", err) - return 1 - } - - netConf, err := libcni.ConfFromBytes(confBytes) - if err != nil { - fmt.Printf("could not parse netconfig: %+v", err) - return 1 - } - fmt.Printf("Parsed network configuration: %+v\n", netConf.Network) - - if len(os.Args) == 1 { - fmt.Printf("Expect CNI plugin paths in argv") - return 1 - } - - targetNs, err := ns.NewNS() - if err != nil { - fmt.Printf("Could not create ns: %+v", err) - return 1 - } - defer targetNs.Close() - - ifName := "eth0" - - runtimeConf := &libcni.RuntimeConf{ - ContainerID: "some-container-id", - NetNS: targetNs.Path(), - IfName: ifName, - } - - cniConfig := &libcni.CNIConfig{Path: os.Args[1:]} - - result, err := cniConfig.AddNetwork(netConf, runtimeConf) - if err != nil { - fmt.Printf("AddNetwork failed: %+v", err) - return 2 - } - fmt.Printf("AddNetwork result: %+v", result) - - expectedIP := result.IP4.IP - - err = targetNs.Do(func(ns.NetNS) error { - netif, err := net.InterfaceByName(ifName) - if err != nil { - return fmt.Errorf("could not retrieve interface: %v", err) - } - - addrs, err := netif.Addrs() - if err != nil { - return fmt.Errorf("could not retrieve addresses, %+v", err) - } - - found := false - for _, addr := range addrs { - if addr.String() == expectedIP.String() { - found = true - break - } - } - - if !found { - return fmt.Errorf("Far-side link did not have expected address %s", expectedIP) - } - return nil - }) - if err != nil { - fmt.Println(err) - return 4 - } - - err = cniConfig.DelNetwork(netConf, runtimeConf) - if err != nil { - fmt.Printf("DelNetwork failed: %v", err) - return 5 - } - - err = targetNs.Do(func(ns.NetNS) error { - _, err := net.InterfaceByName(ifName) - if err == nil { - return fmt.Errorf("interface was not deleted") - } - return nil - }) - if err != nil { - fmt.Println(err) - return 6 - } - - return 0 -} -`, - }, -} diff --git a/pkg/version/legacy_examples/examples.go b/pkg/version/legacy_examples/examples.go deleted file mode 100644 index 1bf406b3..00000000 --- a/pkg/version/legacy_examples/examples.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package legacy_examples contains sample code from prior versions of -// the CNI library, for use in verifying backwards compatibility. -package legacy_examples - -import ( - "io/ioutil" - "net" - "path/filepath" - "sync" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" - "github.com/containernetworking/cni/pkg/version/testhelpers" -) - -// An Example is a Git reference to the CNI repo and a Golang CNI plugin that -// builds against that version of the repo. -// -// By convention, every Example plugin returns an ADD result that is -// semantically equivalent to the ExpectedResult. -type Example struct { - Name string - CNIRepoGitRef string - PluginSource string -} - -var buildDir = "" -var buildDirLock sync.Mutex - -func ensureBuildDirExists() error { - buildDirLock.Lock() - defer buildDirLock.Unlock() - - if buildDir != "" { - return nil - } - - var err error - buildDir, err = ioutil.TempDir("", "cni-example-plugins") - return err -} - -// Build builds the example, returning the path to the binary -func (e Example) Build() (string, error) { - if err := ensureBuildDirExists(); err != nil { - return "", err - } - - outBinPath := filepath.Join(buildDir, e.Name) - - if err := testhelpers.BuildAt([]byte(e.PluginSource), e.CNIRepoGitRef, outBinPath); err != nil { - return "", err - } - return outBinPath, nil -} - -// V010 acts like a CNI plugin from the v0.1.0 era -var V010 = Example{ - Name: "example_v010", - CNIRepoGitRef: "2c482f4", - PluginSource: `package main - -import ( - "net" - - "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/types" -) - -var result = types.Result{ - IP4: &types.IPConfig{ - IP: net.IPNet{ - IP: net.ParseIP("10.1.2.3"), - Mask: net.CIDRMask(24, 32), - }, - Gateway: net.ParseIP("10.1.2.1"), - Routes: []types.Route{ - types.Route{ - Dst: net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - }, - GW: net.ParseIP("10.1.0.1"), - }, - }, - }, - DNS: types.DNS{ - Nameservers: []string{"8.8.8.8"}, - Domain: "example.com", - }, -} - -func c(_ *skel.CmdArgs) error { result.Print(); return nil } - -func main() { skel.PluginMain(c, c) } -`, -} - -// ExpectedResult is the current representation of the plugin result -// that is expected from each of the examples. -// -// As we change the CNI spec, the Result type and this value may change. -// The text of the example plugins should not. -var ExpectedResult = &types020.Result{ - IP4: &types020.IPConfig{ - IP: net.IPNet{ - IP: net.ParseIP("10.1.2.3"), - Mask: net.CIDRMask(24, 32), - }, - Gateway: net.ParseIP("10.1.2.1"), - Routes: []types.Route{ - types.Route{ - Dst: net.IPNet{ - IP: net.ParseIP("0.0.0.0"), - Mask: net.CIDRMask(0, 32), - }, - GW: net.ParseIP("10.1.0.1"), - }, - }, - }, - DNS: types.DNS{ - Nameservers: []string{"8.8.8.8"}, - Domain: "example.com", - }, -} diff --git a/pkg/version/legacy_examples/legacy_examples_suite_test.go b/pkg/version/legacy_examples/legacy_examples_suite_test.go deleted file mode 100644 index a126531d..00000000 --- a/pkg/version/legacy_examples/legacy_examples_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package legacy_examples_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestLegacyExamples(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "LegacyExamples Suite") -} diff --git a/pkg/version/legacy_examples/legacy_examples_test.go b/pkg/version/legacy_examples/legacy_examples_test.go deleted file mode 100644 index 41151056..00000000 --- a/pkg/version/legacy_examples/legacy_examples_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package legacy_examples_test - -import ( - "os" - "path/filepath" - - "github.com/containernetworking/cni/pkg/version/legacy_examples" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("The v0.1.0 Example", func() { - It("builds ok", func() { - example := legacy_examples.V010 - pluginPath, err := example.Build() - Expect(err).NotTo(HaveOccurred()) - - Expect(filepath.Base(pluginPath)).To(Equal(example.Name)) - - Expect(os.RemoveAll(pluginPath)).To(Succeed()) - }) -}) diff --git a/pkg/version/plugin.go b/pkg/version/plugin.go deleted file mode 100644 index 8a467281..00000000 --- a/pkg/version/plugin.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version - -import ( - "encoding/json" - "fmt" - "io" -) - -// PluginInfo reports information about CNI versioning -type PluginInfo interface { - // SupportedVersions returns one or more CNI spec versions that the plugin - // supports. If input is provided in one of these versions, then the plugin - // promises to use the same CNI version in its response - SupportedVersions() []string - - // Encode writes this CNI version information as JSON to the given Writer - Encode(io.Writer) error -} - -type pluginInfo struct { - CNIVersion_ string `json:"cniVersion"` - SupportedVersions_ []string `json:"supportedVersions,omitempty"` -} - -// pluginInfo implements the PluginInfo interface -var _ PluginInfo = &pluginInfo{} - -func (p *pluginInfo) Encode(w io.Writer) error { - return json.NewEncoder(w).Encode(p) -} - -func (p *pluginInfo) SupportedVersions() []string { - return p.SupportedVersions_ -} - -// PluginSupports returns a new PluginInfo that will report the given versions -// as supported -func PluginSupports(supportedVersions ...string) PluginInfo { - if len(supportedVersions) < 1 { - panic("programmer error: you must support at least one version") - } - return &pluginInfo{ - CNIVersion_: Current(), - SupportedVersions_: supportedVersions, - } -} - -// PluginDecoder can decode the response returned by a plugin's VERSION command -type PluginDecoder struct{} - -func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { - var info pluginInfo - err := json.Unmarshal(jsonBytes, &info) - if err != nil { - return nil, fmt.Errorf("decoding version info: %s", err) - } - if info.CNIVersion_ == "" { - return nil, fmt.Errorf("decoding version info: missing field cniVersion") - } - if len(info.SupportedVersions_) == 0 { - if info.CNIVersion_ == "0.2.0" { - return PluginSupports("0.1.0", "0.2.0"), nil - } - return nil, fmt.Errorf("decoding version info: missing field supportedVersions") - } - return &info, nil -} diff --git a/pkg/version/plugin_test.go b/pkg/version/plugin_test.go deleted file mode 100644 index 124288fd..00000000 --- a/pkg/version/plugin_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version_test - -import ( - "github.com/containernetworking/cni/pkg/version" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Decoding versions reported by a plugin", func() { - var ( - decoder *version.PluginDecoder - versionStdout []byte - ) - - BeforeEach(func() { - decoder = &version.PluginDecoder{} - versionStdout = []byte(`{ - "cniVersion": "some-library-version", - "supportedVersions": [ "some-version", "some-other-version" ] - }`) - }) - - It("returns a PluginInfo that represents the given json bytes", func() { - pluginInfo, err := decoder.Decode(versionStdout) - Expect(err).NotTo(HaveOccurred()) - Expect(pluginInfo).NotTo(BeNil()) - Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ - "some-version", - "some-other-version", - })) - }) - - Context("when the bytes cannot be decoded as json", func() { - BeforeEach(func() { - versionStdout = []byte(`{{{`) - }) - - It("returns a meaningful error", func() { - _, err := decoder.Decode(versionStdout) - Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string")) - }) - }) - - Context("when the json bytes are missing the required CNIVersion field", func() { - BeforeEach(func() { - versionStdout = []byte(`{ "supportedVersions": [ "foo" ] }`) - }) - - It("returns a meaningful error", func() { - _, err := decoder.Decode(versionStdout) - Expect(err).To(MatchError("decoding version info: missing field cniVersion")) - }) - }) - - Context("when there are no supported versions", func() { - BeforeEach(func() { - versionStdout = []byte(`{ "cniVersion": "0.2.0" }`) - }) - - It("assumes that the supported versions are 0.1.0 and 0.2.0", func() { - pluginInfo, err := decoder.Decode(versionStdout) - Expect(err).NotTo(HaveOccurred()) - Expect(pluginInfo).NotTo(BeNil()) - Expect(pluginInfo.SupportedVersions()).To(Equal([]string{ - "0.1.0", - "0.2.0", - })) - }) - }) - -}) diff --git a/pkg/version/reconcile.go b/pkg/version/reconcile.go deleted file mode 100644 index 25c3810b..00000000 --- a/pkg/version/reconcile.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version - -import "fmt" - -type ErrorIncompatible struct { - Config string - Supported []string -} - -func (e *ErrorIncompatible) Details() string { - return fmt.Sprintf("config is %q, plugin supports %q", e.Config, e.Supported) -} - -func (e *ErrorIncompatible) Error() string { - return fmt.Sprintf("incompatible CNI versions: %s", e.Details()) -} - -type Reconciler struct{} - -func (r *Reconciler) Check(configVersion string, pluginInfo PluginInfo) *ErrorIncompatible { - return r.CheckRaw(configVersion, pluginInfo.SupportedVersions()) -} - -func (*Reconciler) CheckRaw(configVersion string, supportedVersions []string) *ErrorIncompatible { - for _, supportedVersion := range supportedVersions { - if configVersion == supportedVersion { - return nil - } - } - - return &ErrorIncompatible{ - Config: configVersion, - Supported: supportedVersions, - } -} diff --git a/pkg/version/reconcile_test.go b/pkg/version/reconcile_test.go deleted file mode 100644 index 0c964cea..00000000 --- a/pkg/version/reconcile_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version_test - -import ( - "github.com/containernetworking/cni/pkg/version" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Reconcile versions of net config with versions supported by plugins", func() { - var ( - reconciler *version.Reconciler - pluginInfo version.PluginInfo - ) - - BeforeEach(func() { - reconciler = &version.Reconciler{} - pluginInfo = version.PluginSupports("1.2.3", "4.3.2") - }) - - It("succeeds if the config version is supported by the plugin", func() { - err := reconciler.Check("4.3.2", pluginInfo) - Expect(err).NotTo(HaveOccurred()) - }) - - Context("when the config version is not supported by the plugin", func() { - It("returns a helpful error", func() { - err := reconciler.Check("0.1.0", pluginInfo) - - Expect(err).To(Equal(&version.ErrorIncompatible{ - Config: "0.1.0", - Supported: []string{"1.2.3", "4.3.2"}, - })) - - Expect(err.Error()).To(Equal(`incompatible CNI versions: config is "0.1.0", plugin supports ["1.2.3" "4.3.2"]`)) - }) - }) -}) diff --git a/pkg/version/testhelpers/testhelpers.go b/pkg/version/testhelpers/testhelpers.go deleted file mode 100644 index 773d0120..00000000 --- a/pkg/version/testhelpers/testhelpers.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package testhelpers supports testing of CNI components of different versions -// -// For example, to build a plugin against an old version of the CNI library, -// we can pass the plugin's source and the old git commit reference to BuildAt. -// We could then test how the built binary responds when called by the latest -// version of this library. -package testhelpers - -import ( - "fmt" - "io/ioutil" - "math/rand" - "os" - "os/exec" - "path/filepath" - "strings" - "time" -) - -const packageBaseName = "github.com/containernetworking/cni" - -func run(cmd *exec.Cmd) error { - out, err := cmd.CombinedOutput() - if err != nil { - command := strings.Join(cmd.Args, " ") - return fmt.Errorf("running %q: %s", command, out) - } - return nil -} - -func goBuildEnviron(gopath string) []string { - environ := os.Environ() - for i, kvp := range environ { - if strings.HasPrefix(kvp, "GOPATH=") { - environ[i] = "GOPATH=" + gopath - return environ - } - } - environ = append(environ, "GOPATH="+gopath) - return environ -} - -func buildGoProgram(gopath, packageName, outputFilePath string) error { - cmd := exec.Command("go", "build", "-o", outputFilePath, packageName) - cmd.Env = goBuildEnviron(gopath) - return run(cmd) -} - -func createSingleFilePackage(gopath, packageName string, fileContents []byte) error { - dirName := filepath.Join(gopath, "src", packageName) - err := os.MkdirAll(dirName, 0700) - if err != nil { - return err - } - - return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600) -} - -func removePackage(gopath, packageName string) error { - dirName := filepath.Join(gopath, "src", packageName) - return os.RemoveAll(dirName) -} - -func isRepoRoot(path string) bool { - _, err := ioutil.ReadDir(filepath.Join(path, ".git")) - return (err == nil) && (filepath.Base(path) == "cni") -} - -func LocateCurrentGitRepo() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", err - } - - for i := 0; i < 5; i++ { - if isRepoRoot(dir) { - return dir, nil - } - - dir, err = filepath.Abs(filepath.Dir(dir)) - if err != nil { - return "", fmt.Errorf("abs(dir(%q)): %s", dir, err) - } - } - - return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir) -} - -func gitCloneThisRepo(cloneDestination string) error { - err := os.MkdirAll(cloneDestination, 0700) - if err != nil { - return err - } - - currentGitRepo, err := LocateCurrentGitRepo() - if err != nil { - return err - } - - return run(exec.Command("git", "clone", currentGitRepo, cloneDestination)) -} - -func gitCheckout(localRepo string, gitRef string) error { - return run(exec.Command("git", "-C", localRepo, "checkout", gitRef)) -} - -// BuildAt builds the go programSource using the version of the CNI library -// at gitRef, and saves the resulting binary file at outputFilePath -func BuildAt(programSource []byte, gitRef string, outputFilePath string) error { - tempGoPath, err := ioutil.TempDir("", "cni-git-") - if err != nil { - return err - } - defer os.RemoveAll(tempGoPath) - - cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName) - err = gitCloneThisRepo(cloneDestination) - if err != nil { - return err - } - - err = gitCheckout(cloneDestination, gitRef) - if err != nil { - return err - } - - rand.Seed(time.Now().UnixNano()) - testPackageName := fmt.Sprintf("test-package-%x", rand.Int31()) - - err = createSingleFilePackage(tempGoPath, testPackageName, programSource) - if err != nil { - return err - } - defer removePackage(tempGoPath, testPackageName) - - err = buildGoProgram(tempGoPath, testPackageName, outputFilePath) - if err != nil { - return err - } - - return nil -} diff --git a/pkg/version/testhelpers/testhelpers_suite_test.go b/pkg/version/testhelpers/testhelpers_suite_test.go deleted file mode 100644 index 72f65f9c..00000000 --- a/pkg/version/testhelpers/testhelpers_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testhelpers_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestTesthelpers(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Testhelpers Suite") -} diff --git a/pkg/version/testhelpers/testhelpers_test.go b/pkg/version/testhelpers/testhelpers_test.go deleted file mode 100644 index 3473cd59..00000000 --- a/pkg/version/testhelpers/testhelpers_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package testhelpers_test - -import ( - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - "github.com/containernetworking/cni/pkg/version/testhelpers" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("BuildAt", func() { - var ( - gitRef string - outputFilePath string - outputDir string - programSource []byte - ) - BeforeEach(func() { - programSource = []byte(`package main - -import "github.com/containernetworking/cni/pkg/skel" - -func c(_ *skel.CmdArgs) error { return nil } - -func main() { skel.PluginMain(c, c) } -`) - gitRef = "f4364185253" - - var err error - outputDir, err = ioutil.TempDir("", "bin") - Expect(err).NotTo(HaveOccurred()) - outputFilePath = filepath.Join(outputDir, "some-binary") - }) - - AfterEach(func() { - Expect(os.RemoveAll(outputDir)).To(Succeed()) - }) - - It("builds the provided source code using the CNI library at the given git ref", func() { - Expect(outputFilePath).NotTo(BeAnExistingFile()) - - err := testhelpers.BuildAt(programSource, gitRef, outputFilePath) - Expect(err).NotTo(HaveOccurred()) - - Expect(outputFilePath).To(BeAnExistingFile()) - - cmd := exec.Command(outputFilePath) - cmd.Env = []string{"CNI_COMMAND=VERSION"} - output, err := cmd.CombinedOutput() - Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{})) - Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION")) - }) -}) - -var _ = Describe("LocateCurrentGitRepo", func() { - It("returns the path to the root of the CNI git repo", func() { - path, err := testhelpers.LocateCurrentGitRepo() - Expect(err).NotTo(HaveOccurred()) - - AssertItIsTheCNIRepoRoot(path) - }) - - Context("when run from a different directory", func() { - BeforeEach(func() { - os.Chdir("..") - }) - - It("still finds the CNI repo root", func() { - path, err := testhelpers.LocateCurrentGitRepo() - Expect(err).NotTo(HaveOccurred()) - - AssertItIsTheCNIRepoRoot(path) - }) - }) -}) - -func AssertItIsTheCNIRepoRoot(path string) { - Expect(path).To(BeADirectory()) - files, err := ioutil.ReadDir(path) - Expect(err).NotTo(HaveOccurred()) - - names := []string{} - for _, file := range files { - names = append(names, file.Name()) - } - - Expect(names).To(ContainElement("SPEC.md")) - Expect(names).To(ContainElement("libcni")) - Expect(names).To(ContainElement("cnitool")) -} diff --git a/pkg/version/version.go b/pkg/version/version.go deleted file mode 100644 index efe8ea87..00000000 --- a/pkg/version/version.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version - -import ( - "fmt" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/020" - "github.com/containernetworking/cni/pkg/types/current" -) - -// Current reports the version of the CNI spec implemented by this library -func Current() string { - return "0.3.1" -} - -// Legacy PluginInfo describes a plugin that is backwards compatible with the -// CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0 -// library ought to work correctly with a plugin that reports support for -// Legacy versions. -// -// Any future CNI spec versions which meet this definition should be added to -// this list. -var Legacy = PluginSupports("0.1.0", "0.2.0") -var All = PluginSupports("0.1.0", "0.2.0", "0.3.0", "0.3.1") - -var resultFactories = []struct { - supportedVersions []string - newResult types.ResultFactoryFunc -}{ - {current.SupportedVersions, current.NewResult}, - {types020.SupportedVersions, types020.NewResult}, -} - -// Finds a Result object matching the requested version (if any) and asks -// that object to parse the plugin result, returning an error if parsing failed. -func NewResult(version string, resultBytes []byte) (types.Result, error) { - reconciler := &Reconciler{} - for _, resultFactory := range resultFactories { - err := reconciler.CheckRaw(version, resultFactory.supportedVersions) - if err == nil { - // Result supports this version - return resultFactory.newResult(resultBytes) - } - } - - return nil, fmt.Errorf("unsupported CNI result version %q", version) -} diff --git a/pkg/version/version_suite_test.go b/pkg/version/version_suite_test.go deleted file mode 100644 index 25d503c8..00000000 --- a/pkg/version/version_suite_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestVersion(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Version Suite") -} diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go index a202720d..ca34eec4 100644 --- a/plugins/ipam/dhcp/lease.go +++ b/plugins/ipam/dhcp/lease.go @@ -26,8 +26,8 @@ import ( "github.com/d2g/dhcp4client" "github.com/vishvananda/netlink" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/plugins/pkg/ns" ) // RFC 2131 suggests using exponential backoff, starting with 4sec diff --git a/plugins/ipam/host-local/backend/allocator/allocator.go b/plugins/ipam/host-local/backend/allocator/allocator.go index 87b4fa19..56b8bdf0 100644 --- a/plugins/ipam/host-local/backend/allocator/allocator.go +++ b/plugins/ipam/host-local/backend/allocator/allocator.go @@ -20,9 +20,9 @@ import ( "net" "os" - "github.com/containernetworking/cni/pkg/ip" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ip" "github.com/containernetworking/plugins/plugins/ipam/host-local/backend" ) diff --git a/plugins/ipam/host-local/host_local_test.go b/plugins/ipam/host-local/host_local_test.go index f1578d09..5e2c69c4 100644 --- a/plugins/ipam/host-local/host_local_test.go +++ b/plugins/ipam/host-local/host_local_test.go @@ -23,10 +23,10 @@ import ( "strings" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/testutils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index 876cb5bb..83468d9c 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -22,14 +22,14 @@ import ( "runtime" "syscall" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ipam" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" - "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ipam" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/utils" "github.com/vishvananda/netlink" ) diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 65f083b7..8fe56fc0 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -20,14 +20,13 @@ import ( "strings" "syscall" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" - - "github.com/containernetworking/cni/pkg/utils/hwaddr" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + "github.com/containernetworking/plugins/pkg/utils/hwaddr" "github.com/vishvananda/netlink" diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index 4fba4327..b9c7b1b3 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -20,13 +20,13 @@ import ( "fmt" "runtime" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ipam" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ipam" + "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" ) diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go index 41a56256..a2b92179 100644 --- a/plugins/main/ipvlan/ipvlan_test.go +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -19,11 +19,11 @@ import ( "net" "syscall" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" "github.com/vishvananda/netlink" diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go index 6441ba5c..08c84a5d 100644 --- a/plugins/main/loopback/loopback.go +++ b/plugins/main/loopback/loopback.go @@ -15,10 +15,10 @@ package main import ( - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" ) diff --git a/plugins/main/loopback/loopback_test.go b/plugins/main/loopback/loopback_test.go index f71595b0..c5dec3cb 100644 --- a/plugins/main/loopback/loopback_test.go +++ b/plugins/main/loopback/loopback_test.go @@ -20,7 +20,7 @@ import ( "os/exec" "strings" - "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/plugins/pkg/ns" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index 0d34a4c7..4ee9abb4 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -21,14 +21,14 @@ import ( "net" "runtime" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ipam" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" - "github.com/containernetworking/cni/pkg/utils/sysctl" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ipam" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/utils/sysctl" "github.com/vishvananda/netlink" ) diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index e17b14f3..7ae26f30 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -19,12 +19,12 @@ import ( "net" "syscall" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" - "github.com/containernetworking/cni/pkg/utils/hwaddr" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + "github.com/containernetworking/plugins/pkg/utils/hwaddr" "github.com/vishvananda/netlink" diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index f90bafbc..af93312d 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -24,14 +24,14 @@ import ( "github.com/vishvananda/netlink" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ipam" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" - "github.com/containernetworking/cni/pkg/utils" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ipam" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/utils" ) func init() { diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index b30fd31c..a562af7e 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -15,9 +15,9 @@ package main import ( - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" "github.com/vishvananda/netlink" diff --git a/plugins/main/vlan/vlan.go b/plugins/main/vlan/vlan.go index 56735ee1..b9aa4fee 100644 --- a/plugins/main/vlan/vlan.go +++ b/plugins/main/vlan/vlan.go @@ -20,13 +20,13 @@ import ( "fmt" "runtime" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/ipam" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ip" + "github.com/containernetworking/plugins/pkg/ipam" + "github.com/containernetworking/plugins/pkg/ns" "github.com/vishvananda/netlink" ) diff --git a/plugins/main/vlan/vlan_test.go b/plugins/main/vlan/vlan_test.go index c46aa739..06005bb5 100644 --- a/plugins/main/vlan/vlan_test.go +++ b/plugins/main/vlan/vlan_test.go @@ -19,11 +19,11 @@ import ( "net" "syscall" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" "github.com/vishvananda/netlink" diff --git a/plugins/meta/flannel/flannel_test.go b/plugins/meta/flannel/flannel_test.go index a4a8bc7b..0380729c 100644 --- a/plugins/meta/flannel/flannel_test.go +++ b/plugins/meta/flannel/flannel_test.go @@ -18,9 +18,9 @@ import ( "io/ioutil" "os" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go index 9d8e9002..98a242f6 100644 --- a/plugins/meta/tuning/tuning.go +++ b/plugins/meta/tuning/tuning.go @@ -24,11 +24,11 @@ import ( "path/filepath" "strings" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ns" ) // TuningConf represents the network tuning configuration. diff --git a/plugins/sample/sample_test.go b/plugins/sample/sample_test.go index 9d2688cc..d0b64982 100644 --- a/plugins/sample/sample_test.go +++ b/plugins/sample/sample_test.go @@ -17,9 +17,9 @@ package main import ( "fmt" - "github.com/containernetworking/cni/pkg/ns" "github.com/containernetworking/cni/pkg/skel" - "github.com/containernetworking/cni/pkg/testutils" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) diff --git a/test.sh b/test.sh index 82253f3a..06888aa8 100755 --- a/test.sh +++ b/test.sh @@ -10,7 +10,7 @@ source ./build.sh echo "Running tests" -TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/meta/flannel plugins/main/vlan plugins/sample" +TESTABLE="plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/meta/flannel plugins/main/vlan plugins/sample pkg/ip pkg/ipam pkg/ns pkg/utils pkg/utils/hwaddr pkg/utils/sysctl" # user has not provided PKG override if [ -z "$PKG" ]; then From 4e2f467e212f50b46370cc456a955eff0dcff3f6 Mon Sep 17 00:00:00 2001 From: Casey Callendrello Date: Mon, 22 May 2017 20:32:16 +0200 Subject: [PATCH 134/134] vendor: remove moved cni/pkg/... libraries --- Godeps/Godeps.json | 40 +--- .../containernetworking/cni/pkg/ip/cidr.go | 51 ---- .../cni/pkg/ip/ipforward.go | 31 --- .../containernetworking/cni/pkg/ip/ipmasq.go | 66 ------ .../containernetworking/cni/pkg/ip/link.go | 219 ------------------ .../containernetworking/cni/pkg/ip/route.go | 27 --- .../cni/pkg/ip/route_linux.go | 41 ---- .../cni/pkg/ip/route_unspecified.go | 34 --- .../containernetworking/cni/pkg/ipam/ipam.go | 93 -------- .../containernetworking/cni/pkg/ns/README.md | 34 --- .../containernetworking/cni/pkg/ns/ns.go | 178 -------------- .../cni/pkg/ns/ns_linux.go | 149 ------------ .../cni/pkg/ns/ns_unspecified.go | 36 --- .../cni/pkg/testutils/bad_reader.go | 33 --- .../cni/pkg/testutils/cmd.go | 85 ------- .../cni/pkg/utils/hwaddr/hwaddr.go | 63 ----- .../cni/pkg/utils/sysctl/sysctl_linux.go | 56 ----- .../cni/pkg/utils/utils.go | 41 ---- .../onsi/ginkgo/extensions/table/table.go | 98 ++++++++ .../ginkgo/extensions/table/table_entry.go | 72 ++++++ 20 files changed, 175 insertions(+), 1272 deletions(-) delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/cidr.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/ipforward.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/ipmasq.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/link.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/route.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/route_linux.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ip/route_unspecified.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ipam/ipam.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ns/README.md delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ns/ns.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/ns/ns_unspecified.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/testutils/bad_reader.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/testutils/cmd.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/utils/hwaddr/hwaddr.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go delete mode 100644 vendor/github.com/containernetworking/cni/pkg/utils/utils.go create mode 100644 vendor/github.com/onsi/ginkgo/extensions/table/table.go create mode 100644 vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index fd7fc87a..90fb3ea7 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -11,31 +11,11 @@ "Comment": "v0.5.2", "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/ip", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/ipam", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/ns", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, { "ImportPath": "github.com/containernetworking/cni/pkg/skel", "Comment": "v0.5.2", "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/testutils", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, { "ImportPath": "github.com/containernetworking/cni/pkg/types", "Comment": "v0.5.2", @@ -51,21 +31,6 @@ "Comment": "v0.5.2", "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/utils", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/utils/hwaddr", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, - { - "ImportPath": "github.com/containernetworking/cni/pkg/utils/sysctl", - "Comment": "v0.5.2", - "Rev": "137b4975ecab6e1f0c24c1e3c228a50a3cfba75e" - }, { "ImportPath": "github.com/containernetworking/cni/pkg/version", "Comment": "v0.5.2", @@ -99,6 +64,11 @@ "Comment": "v1.2.0-29-g7f8ab55", "Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700" }, + { + "ImportPath": "github.com/onsi/ginkgo/extensions/table", + "Comment": "v1.2.0-29-g7f8ab55", + "Rev": "7f8ab55aaf3b86885aa55b762e803744d1674700" + }, { "ImportPath": "github.com/onsi/ginkgo/internal/codelocation", "Comment": "v1.2.0-29-g7f8ab55", diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/cidr.go b/vendor/github.com/containernetworking/cni/pkg/ip/cidr.go deleted file mode 100644 index dae2c4d0..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/cidr.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "math/big" - "net" -) - -// NextIP returns IP incremented by 1 -func NextIP(ip net.IP) net.IP { - i := ipToInt(ip) - return intToIP(i.Add(i, big.NewInt(1))) -} - -// PrevIP returns IP decremented by 1 -func PrevIP(ip net.IP) net.IP { - i := ipToInt(ip) - return intToIP(i.Sub(i, big.NewInt(1))) -} - -func ipToInt(ip net.IP) *big.Int { - if v := ip.To4(); v != nil { - return big.NewInt(0).SetBytes(v) - } - return big.NewInt(0).SetBytes(ip.To16()) -} - -func intToIP(i *big.Int) net.IP { - return net.IP(i.Bytes()) -} - -// Network masks off the host portion of the IP -func Network(ipn *net.IPNet) *net.IPNet { - return &net.IPNet{ - IP: ipn.IP.Mask(ipn.Mask), - Mask: ipn.Mask, - } -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/ipforward.go b/vendor/github.com/containernetworking/cni/pkg/ip/ipforward.go deleted file mode 100644 index 77ee7463..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/ipforward.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "io/ioutil" -) - -func EnableIP4Forward() error { - return echo1("/proc/sys/net/ipv4/ip_forward") -} - -func EnableIP6Forward() error { - return echo1("/proc/sys/net/ipv6/conf/all/forwarding") -} - -func echo1(f string) error { - return ioutil.WriteFile(f, []byte("1"), 0644) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/ipmasq.go b/vendor/github.com/containernetworking/cni/pkg/ip/ipmasq.go deleted file mode 100644 index 8ee27971..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/ipmasq.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "fmt" - "net" - - "github.com/coreos/go-iptables/iptables" -) - -// SetupIPMasq installs iptables rules to masquerade traffic -// coming from ipn and going outside of it -func SetupIPMasq(ipn *net.IPNet, chain string, comment string) error { - ipt, err := iptables.New() - if err != nil { - return fmt.Errorf("failed to locate iptables: %v", err) - } - - if err = ipt.NewChain("nat", chain); err != nil { - if err.(*iptables.Error).ExitStatus() != 1 { - // TODO(eyakubovich): assumes exit status 1 implies chain exists - return err - } - } - - if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT", "-m", "comment", "--comment", comment); err != nil { - return err - } - - if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE", "-m", "comment", "--comment", comment); err != nil { - return err - } - - return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment) -} - -// TeardownIPMasq undoes the effects of SetupIPMasq -func TeardownIPMasq(ipn *net.IPNet, chain string, comment string) error { - ipt, err := iptables.New() - if err != nil { - return fmt.Errorf("failed to locate iptables: %v", err) - } - - if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain, "-m", "comment", "--comment", comment); err != nil { - return err - } - - if err = ipt.ClearChain("nat", chain); err != nil { - return err - } - - return ipt.DeleteChain("nat", chain) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/link.go b/vendor/github.com/containernetworking/cni/pkg/ip/link.go deleted file mode 100644 index a9842627..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/link.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "crypto/rand" - "errors" - "fmt" - "net" - "os" - - "github.com/containernetworking/cni/pkg/ns" - "github.com/containernetworking/cni/pkg/utils/hwaddr" - "github.com/vishvananda/netlink" -) - -var ( - ErrLinkNotFound = errors.New("link not found") -) - -func makeVethPair(name, peer string, mtu int) (netlink.Link, error) { - veth := &netlink.Veth{ - LinkAttrs: netlink.LinkAttrs{ - Name: name, - Flags: net.FlagUp, - MTU: mtu, - }, - PeerName: peer, - } - if err := netlink.LinkAdd(veth); err != nil { - return nil, err - } - - return veth, nil -} - -func peerExists(name string) bool { - if _, err := netlink.LinkByName(name); err != nil { - return false - } - return true -} - -func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) { - for i := 0; i < 10; i++ { - peerName, err = RandomVethName() - if err != nil { - return - } - - veth, err = makeVethPair(name, peerName, mtu) - switch { - case err == nil: - return - - case os.IsExist(err): - if peerExists(peerName) { - continue - } - err = fmt.Errorf("container veth name provided (%v) already exists", name) - return - - default: - err = fmt.Errorf("failed to make veth pair: %v", err) - return - } - } - - // should really never be hit - err = fmt.Errorf("failed to find a unique veth name") - return -} - -// RandomVethName returns string "veth" with random prefix (hashed from entropy) -func RandomVethName() (string, error) { - entropy := make([]byte, 4) - _, err := rand.Reader.Read(entropy) - if err != nil { - return "", fmt.Errorf("failed to generate random veth name: %v", err) - } - - // NetworkManager (recent versions) will ignore veth devices that start with "veth" - return fmt.Sprintf("veth%x", entropy), nil -} - -func RenameLink(curName, newName string) error { - link, err := netlink.LinkByName(curName) - if err == nil { - err = netlink.LinkSetName(link, newName) - } - return err -} - -func ifaceFromNetlinkLink(l netlink.Link) net.Interface { - a := l.Attrs() - return net.Interface{ - Index: a.Index, - MTU: a.MTU, - Name: a.Name, - HardwareAddr: a.HardwareAddr, - Flags: a.Flags, - } -} - -// SetupVeth sets up a pair of virtual ethernet devices. -// Call SetupVeth from inside the container netns. It will create both veth -// devices and move the host-side veth into the provided hostNS namespace. -// On success, SetupVeth returns (hostVeth, containerVeth, nil) -func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) { - hostVethName, contVeth, err := makeVeth(contVethName, mtu) - if err != nil { - return net.Interface{}, net.Interface{}, err - } - - if err = netlink.LinkSetUp(contVeth); err != nil { - return net.Interface{}, net.Interface{}, fmt.Errorf("failed to set %q up: %v", contVethName, err) - } - - hostVeth, err := netlink.LinkByName(hostVethName) - if err != nil { - return net.Interface{}, net.Interface{}, fmt.Errorf("failed to lookup %q: %v", hostVethName, err) - } - - if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { - return net.Interface{}, net.Interface{}, fmt.Errorf("failed to move veth to host netns: %v", err) - } - - err = hostNS.Do(func(_ ns.NetNS) error { - hostVeth, err = netlink.LinkByName(hostVethName) - if err != nil { - return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err) - } - - if err = netlink.LinkSetUp(hostVeth); err != nil { - return fmt.Errorf("failed to set %q up: %v", hostVethName, err) - } - return nil - }) - if err != nil { - return net.Interface{}, net.Interface{}, err - } - return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil -} - -// DelLinkByName removes an interface link. -func DelLinkByName(ifName string) error { - iface, err := netlink.LinkByName(ifName) - if err != nil { - return fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - if err = netlink.LinkDel(iface); err != nil { - return fmt.Errorf("failed to delete %q: %v", ifName, err) - } - - return nil -} - -// DelLinkByNameAddr remove an interface returns its IP address -// of the specified family -func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { - iface, err := netlink.LinkByName(ifName) - if err != nil { - if err != nil && err.Error() == "Link not found" { - return nil, ErrLinkNotFound - } - return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - addrs, err := netlink.AddrList(iface, family) - if err != nil || len(addrs) == 0 { - return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err) - } - - if err = netlink.LinkDel(iface); err != nil { - return nil, fmt.Errorf("failed to delete %q: %v", ifName, err) - } - - return addrs[0].IPNet, nil -} - -func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error { - iface, err := netlink.LinkByName(ifName) - if err != nil { - return fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - switch { - case ip4 == nil && ip6 == nil: - return fmt.Errorf("neither ip4 or ip6 specified") - - case ip4 != nil: - { - hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix) - if err != nil { - return fmt.Errorf("failed to generate hardware addr: %v", err) - } - if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil { - return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err) - } - } - case ip6 != nil: - // TODO: IPv6 - } - - return nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/route.go b/vendor/github.com/containernetworking/cni/pkg/ip/route.go deleted file mode 100644 index 1325a47a..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/route.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "net" - - "github.com/vishvananda/netlink" -) - -// AddDefaultRoute sets the default route on the given gateway. -func AddDefaultRoute(gw net.IP, dev netlink.Link) error { - _, defNet, _ := net.ParseCIDR("0.0.0.0/0") - return AddRoute(defNet, gw, dev) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/route_linux.go b/vendor/github.com/containernetworking/cni/pkg/ip/route_linux.go deleted file mode 100644 index 8b11807d..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/route_linux.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2015-2017 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ip - -import ( - "net" - - "github.com/vishvananda/netlink" -) - -// AddRoute adds a universally-scoped route to a device. -func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return netlink.RouteAdd(&netlink.Route{ - LinkIndex: dev.Attrs().Index, - Scope: netlink.SCOPE_UNIVERSE, - Dst: ipn, - Gw: gw, - }) -} - -// AddHostRoute adds a host-scoped route to a device. -func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return netlink.RouteAdd(&netlink.Route{ - LinkIndex: dev.Attrs().Index, - Scope: netlink.SCOPE_HOST, - Dst: ipn, - Gw: gw, - }) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ip/route_unspecified.go b/vendor/github.com/containernetworking/cni/pkg/ip/route_unspecified.go deleted file mode 100644 index 7e79fdef..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ip/route_unspecified.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2015-2017 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// +build !linux - -package ip - -import ( - "net" - - "github.com/containernetworking/cni/pkg/types" - "github.com/vishvananda/netlink" -) - -// AddRoute adds a universally-scoped route to a device. -func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return types.NotImplementedError -} - -// AddHostRoute adds a host-scoped route to a device. -func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { - return types.NotImplementedError -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ipam/ipam.go b/vendor/github.com/containernetworking/cni/pkg/ipam/ipam.go deleted file mode 100644 index b76780f0..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ipam/ipam.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ipam - -import ( - "fmt" - "net" - "os" - - "github.com/containernetworking/cni/pkg/invoke" - "github.com/containernetworking/cni/pkg/ip" - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/types/current" - - "github.com/vishvananda/netlink" -) - -func ExecAdd(plugin string, netconf []byte) (types.Result, error) { - return invoke.DelegateAdd(plugin, netconf) -} - -func ExecDel(plugin string, netconf []byte) error { - return invoke.DelegateDel(plugin, netconf) -} - -// ConfigureIface takes the result of IPAM plugin and -// applies to the ifName interface -func ConfigureIface(ifName string, res *current.Result) error { - if len(res.Interfaces) == 0 { - return fmt.Errorf("no interfaces to configure") - } - - link, err := netlink.LinkByName(ifName) - if err != nil { - return fmt.Errorf("failed to lookup %q: %v", ifName, err) - } - - if err := netlink.LinkSetUp(link); err != nil { - return fmt.Errorf("failed to set %q UP: %v", ifName, err) - } - - var v4gw, v6gw net.IP - for _, ipc := range res.IPs { - if int(ipc.Interface) >= len(res.Interfaces) || res.Interfaces[ipc.Interface].Name != ifName { - // IP address is for a different interface - return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName) - } - - addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""} - if err = netlink.AddrAdd(link, addr); err != nil { - return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err) - } - - gwIsV4 := ipc.Gateway.To4() != nil - if gwIsV4 && v4gw == nil { - v4gw = ipc.Gateway - } else if !gwIsV4 && v6gw == nil { - v6gw = ipc.Gateway - } - } - - for _, r := range res.Routes { - routeIsV4 := r.Dst.IP.To4() != nil - gw := r.GW - if gw == nil { - if routeIsV4 && v4gw != nil { - gw = v4gw - } else if !routeIsV4 && v6gw != nil { - gw = v6gw - } - } - if err = ip.AddRoute(&r.Dst, gw, link); err != nil { - // we skip over duplicate routes as we assume the first one wins - if !os.IsExist(err) { - return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) - } - } - } - - return nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/README.md b/vendor/github.com/containernetworking/cni/pkg/ns/README.md deleted file mode 100644 index 99aed9c8..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ns/README.md +++ /dev/null @@ -1,34 +0,0 @@ -### Namespaces, Threads, and Go -On Linux each OS thread can have a different network namespace. Go's thread scheduling model switches goroutines between OS threads based on OS thread load and whether the goroutine would block other goroutines. This can result in a goroutine switching network namespaces without notice and lead to errors in your code. - -### Namespace Switching -Switching namespaces with the `ns.Set()` method is not recommended without additional strategies to prevent unexpected namespace changes when your goroutines switch OS threads. - -Go provides the `runtime.LockOSThread()` function to ensure a specific goroutine executes on its current OS thread and prevents any other goroutine from running in that thread until the locked one exits. Careful usage of `LockOSThread()` and goroutines can provide good control over which network namespace a given goroutine executes in. - -For example, you cannot rely on the `ns.Set()` namespace being the current namespace after the `Set()` call unless you do two things. First, the goroutine calling `Set()` must have previously called `LockOSThread()`. Second, you must ensure `runtime.UnlockOSThread()` is not called somewhere in-between. You also cannot rely on the initial network namespace remaining the current network namespace if any other code in your program switches namespaces, unless you have already called `LockOSThread()` in that goroutine. Note that `LockOSThread()` prevents the Go scheduler from optimally scheduling goroutines for best performance, so `LockOSThread()` should only be used in small, isolated goroutines that release the lock quickly. - -### Do() The Recommended Thing -The `ns.Do()` method provides control over network namespaces for you by implementing these strategies. All code dependent on a particular network namespace (including the root namespace) should be wrapped in the `ns.Do()` method to ensure the correct namespace is selected for the duration of your code. For example: - -```go -targetNs, err := ns.NewNS() -if err != nil { - return err -} -err = targetNs.Do(func(hostNs ns.NetNS) error { - dummy := &netlink.Dummy{ - LinkAttrs: netlink.LinkAttrs{ - Name: "dummy0", - }, - } - return netlink.LinkAdd(dummy) -}) -``` - -Note this requirement to wrap every network call is very onerous - any libraries you call might call out to network services such as DNS, and all such calls need to be protected after you call `ns.Do()`. The CNI plugins all exit very soon after calling `ns.Do()` which helps to minimize the problem. - -### Further Reading - - https://github.com/golang/go/wiki/LockOSThread - - http://morsmachine.dk/go-scheduler - - https://github.com/containernetworking/cni/issues/262 diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns.go deleted file mode 100644 index c212f489..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ns/ns.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2015 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ns - -import ( - "fmt" - "os" - "runtime" - "sync" - "syscall" -) - -type NetNS interface { - // Executes the passed closure in this object's network namespace, - // attempting to restore the original namespace before returning. - // However, since each OS thread can have a different network namespace, - // and Go's thread scheduling is highly variable, callers cannot - // guarantee any specific namespace is set unless operations that - // require that namespace are wrapped with Do(). Also, no code called - // from Do() should call runtime.UnlockOSThread(), or the risk - // of executing code in an incorrect namespace will be greater. See - // https://github.com/golang/go/wiki/LockOSThread for further details. - Do(toRun func(NetNS) error) error - - // Sets the current network namespace to this object's network namespace. - // Note that since Go's thread scheduling is highly variable, callers - // cannot guarantee the requested namespace will be the current namespace - // after this function is called; to ensure this wrap operations that - // require the namespace with Do() instead. - Set() error - - // Returns the filesystem path representing this object's network namespace - Path() string - - // Returns a file descriptor representing this object's network namespace - Fd() uintptr - - // Cleans up this instance of the network namespace; if this instance - // is the last user the namespace will be destroyed - Close() error -} - -type netNS struct { - file *os.File - mounted bool - closed bool -} - -// netNS implements the NetNS interface -var _ NetNS = &netNS{} - -const ( - // https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h - NSFS_MAGIC = 0x6e736673 - PROCFS_MAGIC = 0x9fa0 -) - -type NSPathNotExistErr struct{ msg string } - -func (e NSPathNotExistErr) Error() string { return e.msg } - -type NSPathNotNSErr struct{ msg string } - -func (e NSPathNotNSErr) Error() string { return e.msg } - -func IsNSorErr(nspath string) error { - stat := syscall.Statfs_t{} - if err := syscall.Statfs(nspath, &stat); err != nil { - if os.IsNotExist(err) { - err = NSPathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)} - } else { - err = fmt.Errorf("failed to Statfs %q: %v", nspath, err) - } - return err - } - - switch stat.Type { - case PROCFS_MAGIC, NSFS_MAGIC: - return nil - default: - return NSPathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} - } -} - -// Returns an object representing the namespace referred to by @path -func GetNS(nspath string) (NetNS, error) { - err := IsNSorErr(nspath) - if err != nil { - return nil, err - } - - fd, err := os.Open(nspath) - if err != nil { - return nil, err - } - - return &netNS{file: fd}, nil -} - -func (ns *netNS) Path() string { - return ns.file.Name() -} - -func (ns *netNS) Fd() uintptr { - return ns.file.Fd() -} - -func (ns *netNS) errorIfClosed() error { - if ns.closed { - return fmt.Errorf("%q has already been closed", ns.file.Name()) - } - return nil -} - -func (ns *netNS) Do(toRun func(NetNS) error) error { - if err := ns.errorIfClosed(); err != nil { - return err - } - - containedCall := func(hostNS NetNS) error { - threadNS, err := GetCurrentNS() - if err != nil { - return fmt.Errorf("failed to open current netns: %v", err) - } - defer threadNS.Close() - - // switch to target namespace - if err = ns.Set(); err != nil { - return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err) - } - defer threadNS.Set() // switch back - - return toRun(hostNS) - } - - // save a handle to current network namespace - hostNS, err := GetCurrentNS() - if err != nil { - return fmt.Errorf("Failed to open current namespace: %v", err) - } - defer hostNS.Close() - - var wg sync.WaitGroup - wg.Add(1) - - var innerError error - go func() { - defer wg.Done() - runtime.LockOSThread() - innerError = containedCall(hostNS) - }() - wg.Wait() - - return innerError -} - -// WithNetNSPath executes the passed closure under the given network -// namespace, restoring the original namespace afterwards. -func WithNetNSPath(nspath string, toRun func(NetNS) error) error { - ns, err := GetNS(nspath) - if err != nil { - return err - } - defer ns.Close() - return ns.Do(toRun) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go deleted file mode 100644 index c9e1b4f0..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ns/ns_linux.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2015-2017 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ns - -import ( - "crypto/rand" - "fmt" - "os" - "path" - "runtime" - "sync" - - "golang.org/x/sys/unix" -) - -// Returns an object representing the current OS thread's network namespace -func GetCurrentNS() (NetNS, error) { - return GetNS(getCurrentThreadNetNSPath()) -} - -func getCurrentThreadNetNSPath() string { - // /proc/self/ns/net returns the namespace of the main thread, not - // of whatever thread this goroutine is running on. Make sure we - // use the thread's net namespace since the thread is switching around - return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) -} - -// Creates a new persistent network namespace and returns an object -// representing that namespace, without switching to it -func NewNS() (NetNS, error) { - const nsRunDir = "/var/run/netns" - - b := make([]byte, 16) - _, err := rand.Reader.Read(b) - if err != nil { - return nil, fmt.Errorf("failed to generate random netns name: %v", err) - } - - err = os.MkdirAll(nsRunDir, 0755) - if err != nil { - return nil, err - } - - // create an empty file at the mount point - nsName := fmt.Sprintf("cni-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) - nsPath := path.Join(nsRunDir, nsName) - mountPointFd, err := os.Create(nsPath) - if err != nil { - return nil, err - } - mountPointFd.Close() - - // Ensure the mount point is cleaned up on errors; if the namespace - // was successfully mounted this will have no effect because the file - // is in-use - defer os.RemoveAll(nsPath) - - var wg sync.WaitGroup - wg.Add(1) - - // do namespace work in a dedicated goroutine, so that we can safely - // Lock/Unlock OSThread without upsetting the lock/unlock state of - // the caller of this function - var fd *os.File - go (func() { - defer wg.Done() - runtime.LockOSThread() - - var origNS NetNS - origNS, err = GetNS(getCurrentThreadNetNSPath()) - if err != nil { - return - } - defer origNS.Close() - - // create a new netns on the current thread - err = unix.Unshare(unix.CLONE_NEWNET) - if err != nil { - return - } - defer origNS.Set() - - // bind mount the new netns from the current thread onto the mount point - err = unix.Mount(getCurrentThreadNetNSPath(), nsPath, "none", unix.MS_BIND, "") - if err != nil { - return - } - - fd, err = os.Open(nsPath) - if err != nil { - return - } - })() - wg.Wait() - - if err != nil { - unix.Unmount(nsPath, unix.MNT_DETACH) - return nil, fmt.Errorf("failed to create namespace: %v", err) - } - - return &netNS{file: fd, mounted: true}, nil -} - -func (ns *netNS) Close() error { - if err := ns.errorIfClosed(); err != nil { - return err - } - - if err := ns.file.Close(); err != nil { - return fmt.Errorf("Failed to close %q: %v", ns.file.Name(), err) - } - ns.closed = true - - if ns.mounted { - if err := unix.Unmount(ns.file.Name(), unix.MNT_DETACH); err != nil { - return fmt.Errorf("Failed to unmount namespace %s: %v", ns.file.Name(), err) - } - if err := os.RemoveAll(ns.file.Name()); err != nil { - return fmt.Errorf("Failed to clean up namespace %s: %v", ns.file.Name(), err) - } - ns.mounted = false - } - - return nil -} - -func (ns *netNS) Set() error { - if err := ns.errorIfClosed(); err != nil { - return err - } - - if _, _, err := unix.Syscall(unix.SYS_SETNS, ns.Fd(), uintptr(unix.CLONE_NEWNET), 0); err != 0 { - return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) - } - - return nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/ns/ns_unspecified.go b/vendor/github.com/containernetworking/cni/pkg/ns/ns_unspecified.go deleted file mode 100644 index 41b44686..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/ns/ns_unspecified.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2015-2017 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// +build !linux - -package ns - -import "github.com/containernetworking/cni/pkg/types" - -// Returns an object representing the current OS thread's network namespace -func GetCurrentNS() (NetNS, error) { - return nil, types.NotImplementedError -} - -func NewNS() (NetNS, error) { - return nil, types.NotImplementedError -} - -func (ns *netNS) Close() error { - return types.NotImplementedError -} - -func (ns *netNS) Set() error { - return types.NotImplementedError -} diff --git a/vendor/github.com/containernetworking/cni/pkg/testutils/bad_reader.go b/vendor/github.com/containernetworking/cni/pkg/testutils/bad_reader.go deleted file mode 100644 index f9d0aded..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/testutils/bad_reader.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testutils - -import "errors" - -// BadReader is an io.Reader which always errors -type BadReader struct { - Error error -} - -func (r *BadReader) Read(buffer []byte) (int, error) { - if r.Error != nil { - return 0, r.Error - } - return 0, errors.New("banana") -} - -func (r *BadReader) Close() error { - return nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/testutils/cmd.go b/vendor/github.com/containernetworking/cni/pkg/testutils/cmd.go deleted file mode 100644 index a6045b31..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/testutils/cmd.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testutils - -import ( - "io/ioutil" - "os" - - "github.com/containernetworking/cni/pkg/types" - "github.com/containernetworking/cni/pkg/version" -) - -func envCleanup() { - os.Unsetenv("CNI_COMMAND") - os.Unsetenv("CNI_PATH") - os.Unsetenv("CNI_NETNS") - os.Unsetenv("CNI_IFNAME") -} - -func CmdAddWithResult(cniNetns, cniIfname string, conf []byte, f func() error) (types.Result, []byte, error) { - os.Setenv("CNI_COMMAND", "ADD") - os.Setenv("CNI_PATH", os.Getenv("PATH")) - os.Setenv("CNI_NETNS", cniNetns) - os.Setenv("CNI_IFNAME", cniIfname) - defer envCleanup() - - // Redirect stdout to capture plugin result - oldStdout := os.Stdout - r, w, err := os.Pipe() - if err != nil { - return nil, nil, err - } - - os.Stdout = w - err = f() - w.Close() - - var out []byte - if err == nil { - out, err = ioutil.ReadAll(r) - } - os.Stdout = oldStdout - - // Return errors after restoring stdout so Ginkgo will correctly - // emit verbose error information on stdout - if err != nil { - return nil, nil, err - } - - // Plugin must return result in same version as specified in netconf - versionDecoder := &version.ConfigDecoder{} - confVersion, err := versionDecoder.Decode(conf) - if err != nil { - return nil, nil, err - } - - result, err := version.NewResult(confVersion, out) - if err != nil { - return nil, nil, err - } - - return result, out, nil -} - -func CmdDelWithResult(cniNetns, cniIfname string, f func() error) error { - os.Setenv("CNI_COMMAND", "DEL") - os.Setenv("CNI_PATH", os.Getenv("PATH")) - os.Setenv("CNI_NETNS", cniNetns) - os.Setenv("CNI_IFNAME", cniIfname) - defer envCleanup() - - return f() -} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/hwaddr/hwaddr.go b/vendor/github.com/containernetworking/cni/pkg/utils/hwaddr/hwaddr.go deleted file mode 100644 index aaf3b8a0..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/utils/hwaddr/hwaddr.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package hwaddr - -import ( - "fmt" - "net" -) - -const ( - ipRelevantByteLen = 4 - PrivateMACPrefixString = "0a:58" -) - -var ( - // private mac prefix safe to use - PrivateMACPrefix = []byte{0x0a, 0x58} -) - -type SupportIp4OnlyErr struct{ msg string } - -func (e SupportIp4OnlyErr) Error() string { return e.msg } - -type MacParseErr struct{ msg string } - -func (e MacParseErr) Error() string { return e.msg } - -type InvalidPrefixLengthErr struct{ msg string } - -func (e InvalidPrefixLengthErr) Error() string { return e.msg } - -// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. -func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) { - switch { - - case ip.To4() == nil: - return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"} - - case len(prefix) != len(PrivateMACPrefix): - return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf( - "Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)), - } - } - - ipByteLen := len(ip) - return (net.HardwareAddr)( - append( - prefix, - ip[ipByteLen-ipRelevantByteLen:ipByteLen]...), - ), nil -} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go b/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go deleted file mode 100644 index fe06d2d9..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/utils/sysctl/sysctl_linux.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sysctl - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "strings" -) - -// Sysctl provides a method to set/get values from /proc/sys - in linux systems -// new interface to set/get values of variables formerly handled by sysctl syscall -// If optional `params` have only one string value - this function will -// set this value into corresponding sysctl variable -func Sysctl(name string, params ...string) (string, error) { - if len(params) > 1 { - return "", fmt.Errorf("unexcepted additional parameters") - } else if len(params) == 1 { - return setSysctl(name, params[0]) - } - return getSysctl(name) -} - -func getSysctl(name string) (string, error) { - fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) - fullName = filepath.Clean(fullName) - data, err := ioutil.ReadFile(fullName) - if err != nil { - return "", err - } - - return string(data[:len(data)-1]), nil -} - -func setSysctl(name, value string) (string, error) { - fullName := filepath.Join("/proc/sys", strings.Replace(name, ".", "/", -1)) - fullName = filepath.Clean(fullName) - if err := ioutil.WriteFile(fullName, []byte(value), 0644); err != nil { - return "", err - } - - return getSysctl(name) -} diff --git a/vendor/github.com/containernetworking/cni/pkg/utils/utils.go b/vendor/github.com/containernetworking/cni/pkg/utils/utils.go deleted file mode 100644 index 33a2aa79..00000000 --- a/vendor/github.com/containernetworking/cni/pkg/utils/utils.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2016 CNI authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package utils - -import ( - "crypto/sha512" - "fmt" -) - -const ( - maxChainLength = 28 - chainPrefix = "CNI-" - prefixLength = len(chainPrefix) -) - -// Generates a chain name to be used with iptables. -// Ensures that the generated chain name is exactly -// maxChainLength chars in length -func FormatChainName(name string, id string) string { - chainBytes := sha512.Sum512([]byte(name + id)) - chain := fmt.Sprintf("%s%x", chainPrefix, chainBytes) - return chain[:maxChainLength] -} - -// FormatComment returns a comment used for easier -// rule identification within iptables. -func FormatComment(name string, id string) string { - return fmt.Sprintf("name: %q id: %q", name, id) -} diff --git a/vendor/github.com/onsi/ginkgo/extensions/table/table.go b/vendor/github.com/onsi/ginkgo/extensions/table/table.go new file mode 100644 index 00000000..ae8ab7d2 --- /dev/null +++ b/vendor/github.com/onsi/ginkgo/extensions/table/table.go @@ -0,0 +1,98 @@ +/* + +Table provides a simple DSL for Ginkgo-native Table-Driven Tests + +The godoc documentation describes Table's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/ginkgo#table-driven-tests + +*/ + +package table + +import ( + "fmt" + "reflect" + + "github.com/onsi/ginkgo" +) + +/* +DescribeTable describes a table-driven test. + +For example: + + DescribeTable("a simple table", + func(x int, y int, expected bool) { + Ω(x > y).Should(Equal(expected)) + }, + Entry("x > y", 1, 0, true), + Entry("x == y", 0, 0, false), + Entry("x < y", 0, 1, false), + ) + +The first argument to `DescribeTable` is a string description. +The second argument is a function that will be run for each table entry. Your assertions go here - the function is equivalent to a Ginkgo It. +The subsequent arguments must be of type `TableEntry`. We recommend using the `Entry` convenience constructors. + +The `Entry` constructor takes a string description followed by an arbitrary set of parameters. These parameters are passed into your function. + +Under the hood, `DescribeTable` simply generates a new Ginkgo `Describe`. Each `Entry` is turned into an `It` within the `Describe`. + +It's important to understand that the `Describe`s and `It`s are generated at evaluation time (i.e. when Ginkgo constructs the tree of tests and before the tests run). + +Individual Entries can be focused (with FEntry) or marked pending (with PEntry or XEntry). In addition, the entire table can be focused or marked pending with FDescribeTable and PDescribeTable/XDescribeTable. +*/ +func DescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, false, false) + return true +} + +/* +You can focus a table with `FDescribeTable`. This is equivalent to `FDescribe`. +*/ +func FDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, false, true) + return true +} + +/* +You can mark a table as pending with `PDescribeTable`. This is equivalent to `PDescribe`. +*/ +func PDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, true, false) + return true +} + +/* +You can mark a table as pending with `XDescribeTable`. This is equivalent to `XDescribe`. +*/ +func XDescribeTable(description string, itBody interface{}, entries ...TableEntry) bool { + describeTable(description, itBody, entries, true, false) + return true +} + +func describeTable(description string, itBody interface{}, entries []TableEntry, pending bool, focused bool) { + itBodyValue := reflect.ValueOf(itBody) + if itBodyValue.Kind() != reflect.Func { + panic(fmt.Sprintf("DescribeTable expects a function, got %#v", itBody)) + } + + if pending { + ginkgo.PDescribe(description, func() { + for _, entry := range entries { + entry.generateIt(itBodyValue) + } + }) + } else if focused { + ginkgo.FDescribe(description, func() { + for _, entry := range entries { + entry.generateIt(itBodyValue) + } + }) + } else { + ginkgo.Describe(description, func() { + for _, entry := range entries { + entry.generateIt(itBodyValue) + } + }) + } +} diff --git a/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go b/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go new file mode 100644 index 00000000..a6a9e3cc --- /dev/null +++ b/vendor/github.com/onsi/ginkgo/extensions/table/table_entry.go @@ -0,0 +1,72 @@ +package table + +import ( + "reflect" + + "github.com/onsi/ginkgo" +) + +/* +TableEntry represents an entry in a table test. You generally use the `Entry` constructor. +*/ +type TableEntry struct { + Description string + Parameters []interface{} + Pending bool + Focused bool +} + +func (t TableEntry) generateIt(itBody reflect.Value) { + if t.Pending { + ginkgo.PIt(t.Description) + return + } + + values := []reflect.Value{} + for _, param := range t.Parameters { + values = append(values, reflect.ValueOf(param)) + } + + body := func() { + itBody.Call(values) + } + + if t.Focused { + ginkgo.FIt(t.Description, body) + } else { + ginkgo.It(t.Description, body) + } +} + +/* +Entry constructs a TableEntry. + +The first argument is a required description (this becomes the content of the generated Ginkgo `It`). +Subsequent parameters are saved off and sent to the callback passed in to `DescribeTable`. + +Each Entry ends up generating an individual Ginkgo It. +*/ +func Entry(description string, parameters ...interface{}) TableEntry { + return TableEntry{description, parameters, false, false} +} + +/* +You can focus a particular entry with FEntry. This is equivalent to FIt. +*/ +func FEntry(description string, parameters ...interface{}) TableEntry { + return TableEntry{description, parameters, false, true} +} + +/* +You can mark a particular entry as pending with PEntry. This is equivalent to PIt. +*/ +func PEntry(description string, parameters ...interface{}) TableEntry { + return TableEntry{description, parameters, true, false} +} + +/* +You can mark a particular entry as pending with XEntry. This is equivalent to XIt. +*/ +func XEntry(description string, parameters ...interface{}) TableEntry { + return TableEntry{description, parameters, true, false} +}