spec/plugins: return interface details and multiple IP addresses to runtime
authorDan Williams <dcbw@redhat.com>
Tue, 22 Nov 2016 17:32:35 +0000 (11:32 -0600)
committerDan Williams <dcbw@redhat.com>
Wed, 25 Jan 2017 17:31:18 +0000 (11:31 -0600)
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.

14 files changed:
invoke/exec_test.go
invoke/raw_exec_test.go
ipam/ipam.go
ipam/ipam_test.go
skel/skel_test.go
types/020/types.go [new file with mode: 0644]
types/020/types_suite_test.go [new file with mode: 0644]
types/020/types_test.go [new file with mode: 0644]
types/current/types.go
types/current/types_suite_test.go
types/current/types_test.go
types/types.go
version/legacy_examples/examples.go
version/version.go

index 3e207c1..7e804ab 100644 (file)
@@ -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() {
index b0ca960..5ab23ae 100644 (file)
@@ -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{}
        })
 
index 8dd861a..b76780f 100644 (file)
@@ -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
index 622e4c8..2d27825 100644 (file)
@@ -94,20 +94,36 @@ var _ = Describe("IPAM Operations", func() {
                Expect(ipgw6).NotTo(BeNil())
 
                result = &current.Result{
-                       IP4: &current.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: &current.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))
+
+                       var found bool
+                       for _, a := range v6addrs {
+                               if ipNetEqual(a.IPNet, ipv6) {
+                                       found = true
+                                       break
+                               }
+                       }
+                       Expect(found).To(Equal(true))
 
-                       // 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(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)
index 6652fcd..d7f729f 100644 (file)
@@ -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 (file)
index 0000000..666cfe9
--- /dev/null
@@ -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 (file)
index 0000000..095d73e
--- /dev/null
@@ -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 (file)
index 0000000..1bcdda7
--- /dev/null
@@ -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"
+        ]
+    }
+}`))
+       })
+})
index 338b3fd..e686a9a 100644 (file)
@@ -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
+// 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      net.IPNet
-       Gateway net.IP
-       Routes  []types.Route
+       // IP version, either "4" or "6"
+       Version string
+       // Index into Result structs Interfaces list
+       Interface int
+       Address   net.IPNet
+       Gateway   net.IP
 }
 
-// net.IPNet is not JSON (un)marshallable so this duality is needed
-// for our custom IPNet type
+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
 }
index 42a47a2..89cccec 100644 (file)
@@ -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")
 }
index 3810999..f839cc9 100644 (file)
@@ -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())
-
-               // Set every field of the struct to ensure source compatibility
-               res := current.Result{
-                       IP4: &current.IPConfig{
-                               IP:      *ipv4,
-                               Gateway: net.ParseIP("1.2.3.1"),
-                               Routes: []types.Route{
-                                       {Dst: *routev4, GW: routegwv4},
-                               },
+       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 &current.Result{
+               Interfaces: []*current.Interface{
+                       {
+                               Name:    "eth0",
+                               Mac:     "00:11:22:33:44:55",
+                               Sandbox: "/proc/3553/ns/net",
                        },
-                       IP6: &current.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"},
+               },
+       }
+}
+
+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("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("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() {
         ]
     }
 }`))
+               })
        })
 })
index 2ceffeb..a81ac70 100644 (file)
@@ -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 (
index 8b079a3..1bf406b 100644 (file)
@@ -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 = &current.Result{
-       IP4: &current.IPConfig{
+var ExpectedResult = &types020.Result{
+       IP4: &types020.IPConfig{
                IP: net.IPNet{
                        IP:   net.ParseIP("10.1.2.3"),
                        Mask: net.CIDRMask(24, 32),
index e777e52..7c58963 100644 (file)
@@ -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