types: make Result an interface and move existing Result to separate package
authorDan Williams <dcbw@redhat.com>
Wed, 9 Nov 2016 21:11:18 +0000 (15:11 -0600)
committerDan Williams <dcbw@redhat.com>
Wed, 25 Jan 2017 17:31:18 +0000 (11:31 -0600)
29 files changed:
libcni/api.go
libcni/api_test.go
pkg/invoke/delegate.go
pkg/invoke/exec.go
pkg/invoke/exec_test.go
pkg/ipam/ipam.go
pkg/testutils/cmd.go
pkg/types/current/types.go [new file with mode: 0644]
pkg/types/current/types_suite_test.go [new file with mode: 0644]
pkg/types/current/types_test.go [new file with mode: 0644]
pkg/types/types.go
pkg/version/legacy_examples/examples.go
pkg/version/reconcile.go
pkg/version/reconcile_test.go
pkg/version/version.go
plugins/ipam/dhcp/daemon.go
plugins/ipam/dhcp/main.go
plugins/ipam/host-local/backend/allocator/allocator.go
plugins/ipam/host-local/backend/allocator/allocator_test.go
plugins/ipam/host-local/host_local_test.go
plugins/ipam/host-local/main.go
plugins/main/bridge/bridge.go
plugins/main/ipvlan/ipvlan.go
plugins/main/loopback/loopback.go
plugins/main/macvlan/macvlan.go
plugins/main/ptp/ptp.go
plugins/meta/tuning/tuning.go
plugins/test/noop/main.go
test

index 9a82dc3..3bbe46b 100644 (file)
@@ -42,10 +42,10 @@ type NetworkConfigList struct {
 }
 
 type CNI interface {
-       AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (*types.Result, error)
+       AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
        DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error
 
-       AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error)
+       AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
        DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
 }
 
@@ -56,7 +56,7 @@ type CNIConfig struct {
 // CNIConfig implements the CNI interface
 var _ CNI = &CNIConfig{}
 
-func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult *types.Result) (*NetworkConfig, error) {
+func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult types.Result) (*NetworkConfig, error) {
        var err error
 
        // Ensure every config uses the same name and version
@@ -81,8 +81,8 @@ func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult *ty
 }
 
 // AddNetworkList executes a sequence of plugins with the ADD command
-func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (*types.Result, error) {
-       var prevResult *types.Result
+func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
+       var prevResult types.Result
        for _, net := range list.Plugins {
                pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
                if err != nil {
@@ -127,7 +127,7 @@ func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) err
 }
 
 // AddNetwork executes the plugin with the ADD command
-func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) {
+func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error) {
        pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
        if err != nil {
                return nil, err
index bc9f06d..ce1070f 100644 (file)
@@ -24,6 +24,7 @@ import (
        "github.com/containernetworking/cni/libcni"
        "github.com/containernetworking/cni/pkg/skel"
        "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
        noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
 
        . "github.com/onsi/ginkgo"
@@ -116,11 +117,14 @@ var _ = Describe("Invoking plugins", func() {
 
                Describe("AddNetwork", func() {
                        It("executes the plugin with command ADD", func() {
-                               result, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
+                               r, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
                                Expect(err).NotTo(HaveOccurred())
 
-                               Expect(result).To(Equal(&types.Result{
-                                       IP4: &types.IPConfig{
+                               result, err := current.GetResult(r)
+                               Expect(err).NotTo(HaveOccurred())
+
+                               Expect(result).To(Equal(&current.Result{
+                                       IP4: &current.IPConfig{
                                                IP: net.IPNet{
                                                        IP:   net.ParseIP("10.1.2.3"),
                                                        Mask: net.IPv4Mask(255, 255, 255, 0),
@@ -263,12 +267,15 @@ var _ = Describe("Invoking plugins", func() {
 
                Describe("AddNetworkList", func() {
                        It("executes all plugins with command ADD and returns an intermediate result", func() {
-                               result, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig)
+                               r, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig)
+                               Expect(err).NotTo(HaveOccurred())
+
+                               result, err := current.GetResult(r)
                                Expect(err).NotTo(HaveOccurred())
 
-                               Expect(result).To(Equal(&types.Result{
+                               Expect(result).To(Equal(&current.Result{
                                        // IP4 added by first plugin
-                                       IP4: &types.IPConfig{
+                                       IP4: &current.IPConfig{
                                                IP: net.IPNet{
                                                        IP:   net.ParseIP("10.1.2.3"),
                                                        Mask: net.IPv4Mask(255, 255, 255, 0),
index ddf1d17..f25bedd 100644 (file)
@@ -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")
        }
index 167d38f..fc47e7c 100644 (file)
@@ -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 {
index 2b9c9bf..3e207c1 100644 (file)
@@ -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"))
                })
index d9fbff7..8dd861a 100644 (file)
@@ -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)
index 0118f61..5883c08 100644 (file)
 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/pkg/types/current/types.go b/pkg/types/current/types.go
new file mode 100644 (file)
index 0000000..338b3fd
--- /dev/null
@@ -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/pkg/types/current/types_suite_test.go b/pkg/types/current/types_suite_test.go
new file mode 100644 (file)
index 0000000..42a47a2
--- /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 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/pkg/types/current/types_test.go b/pkg/types/current/types_test.go
new file mode 100644 (file)
index 0000000..3810999
--- /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 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: &current.IPConfig{
+                               IP:      *ipv4,
+                               Gateway: net.ParseIP("1.2.3.1"),
+                               Routes: []types.Route{
+                                       {Dst: *routev4, GW: routegwv4},
+                               },
+                       },
+                       IP6: &current.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 c1fddcd..2ceffeb 100644 (file)
@@ -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)
 
-func (r *Result) Print() error {
-       return prettyPrint(r)
-}
+// 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
 
-// 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)
+       // 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
 }
 
-// IPConfig contains values necessary to configure an interface
-type IPConfig struct {
-       IP      net.IPNet
-       Gateway net.IP
-       Routes  []Route
+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
@@ -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 {
index 5716231..8b079a3 100644 (file)
@@ -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 = &current.Result{
+       IP4: &current.IPConfig{
                IP: net.IPNet{
                        IP:   net.ParseIP("10.1.2.3"),
                        Mask: net.CIDRMask(24, 32),
index f61ef65..25c3810 100644 (file)
@@ -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,
        }
 }
index 19a9e23..0c964ce 100644 (file)
@@ -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"]`))
index e39c3b5..e777e52 100644 (file)
 
 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)
+}
index 2386f9a..2910a41 100644 (file)
@@ -29,6 +29,7 @@ import (
 
        "github.com/containernetworking/cni/pkg/skel"
        "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
        "github.com/coreos/go-systemd/activation"
 )
 
@@ -50,7 +51,7 @@ func newDHCP() *DHCP {
 
 // Allocate acquires an IP from a DHCP server for a specified container.
 // The acquired lease will be maintained until Release() is called.
-func (d *DHCP) Allocate(args *skel.CmdArgs, result *types.Result) error {
+func (d *DHCP) Allocate(args *skel.CmdArgs, result *current.Result) error {
        conf := types.NetConf{}
        if err := json.Unmarshal(args.StdinData, &conf); err != nil {
                return fmt.Errorf("error parsing netconf: %v", err)
@@ -70,7 +71,7 @@ func (d *DHCP) Allocate(args *skel.CmdArgs, result *types.Result) error {
 
        d.setLease(args.ContainerID, conf.Name, l)
 
-       result.IP4 = &types.IPConfig{
+       result.IP4 = &current.IPConfig{
                IP:      *ipn,
                Gateway: l.Gateway(),
                Routes:  l.Routes(),
index 0e46af9..f43ad3e 100644 (file)
@@ -21,7 +21,7 @@ import (
        "path/filepath"
 
        "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"
 )
 
@@ -36,8 +36,8 @@ func main() {
 }
 
 func cmdAdd(args *skel.CmdArgs) error {
-       result := types.Result{}
-       if err := rpcCall("DHCP.Allocate", args, &result); err != nil {
+       result := &current.Result{}
+       if err := rpcCall("DHCP.Allocate", args, result); err != nil {
                return err
        }
        return result.Print()
index 6e18172..a6f2c65 100644 (file)
@@ -20,7 +20,7 @@ import (
        "net"
 
        "github.com/containernetworking/cni/pkg/ip"
-       "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
        "github.com/containernetworking/cni/plugins/ipam/host-local/backend"
 )
 
@@ -129,7 +129,7 @@ func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) erro
 }
 
 // Returns newly allocated IP along with its config
-func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
+func (a *IPAllocator) Get(id string) (*current.IPConfig, error) {
        a.store.Lock()
        defer a.store.Unlock()
 
@@ -163,7 +163,7 @@ func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
                }
 
                if reserved {
-                       return &types.IPConfig{
+                       return &current.IPConfig{
                                IP:      net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask},
                                Gateway: gw,
                                Routes:  a.conf.Routes,
@@ -184,7 +184,7 @@ func (a *IPAllocator) Get(id string) (*types.IPConfig, error) {
                        return nil, err
                }
                if reserved {
-                       return &types.IPConfig{
+                       return &current.IPConfig{
                                IP:      net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask},
                                Gateway: gw,
                                Routes:  a.conf.Routes,
index 44a60ba..24fa277 100644 (file)
@@ -17,6 +17,7 @@ package allocator
 import (
        "fmt"
        "github.com/containernetworking/cni/pkg/types"
+       "github.com/containernetworking/cni/pkg/types/current"
        fakestore "github.com/containernetworking/cni/plugins/ipam/host-local/backend/testing"
        . "github.com/onsi/ginkgo"
        . "github.com/onsi/gomega"
@@ -30,7 +31,7 @@ type AllocatorTestCase struct {
        lastIP       string
 }
 
-func (t AllocatorTestCase) run() (*types.IPConfig, error) {
+func (t AllocatorTestCase) run() (*current.IPConfig, error) {
        subnet, err := types.ParseCIDR(t.subnet)
        if err != nil {
                return nil, err
index 2aca1f2..a8742fa 100644 (file)
@@ -24,6 +24,7 @@ import (
        "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/onsi/ginkgo"
        . "github.com/onsi/gomega"
@@ -62,11 +63,14 @@ var _ = Describe("host-local Operations", func() {
                }
 
                // Allocate the IP
-               result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+               r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
                        return cmdAdd(args)
                })
                Expect(err).NotTo(HaveOccurred())
 
+               result, err := current.GetResult(r)
+               Expect(err).NotTo(HaveOccurred())
+
                expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
                Expect(err).NotTo(HaveOccurred())
                expectedAddress.IP = expectedAddress.IP.To16()
@@ -124,11 +128,14 @@ var _ = Describe("host-local Operations", func() {
                }
 
                // Allocate the IP
-               result, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+               r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
                        return cmdAdd(args)
                })
                Expect(err).NotTo(HaveOccurred())
 
+               result, err := current.GetResult(r)
+               Expect(err).NotTo(HaveOccurred())
+
                ipFilePath := filepath.Join(tmpDir, "mynet", result.IP4.IP.IP.String())
                contents, err := ioutil.ReadFile(ipFilePath)
                Expect(err).NotTo(HaveOccurred())
index 287c0a0..a5dd080 100644 (file)
@@ -19,7 +19,7 @@ import (
        "github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk"
 
        "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"
 )
 
@@ -33,7 +33,7 @@ func cmdAdd(args *skel.CmdArgs) error {
                return err
        }
 
-       r := types.Result{}
+       r := &current.Result{}
 
        if ipamConf.ResolvConf != "" {
                dns, err := parseResolvConf(ipamConf.ResolvConf)
@@ -54,11 +54,10 @@ func cmdAdd(args *skel.CmdArgs) error {
                return err
        }
 
-       ipConf, err := allocator.Get(args.ContainerID)
+       r.IP4, err = allocator.Get(args.ContainerID)
        if err != nil {
                return err
        }
-       r.IP4 = ipConf
 
        return r.Print()
 }
index bc17301..a2f8b17 100644 (file)
@@ -27,6 +27,7 @@ import (
        "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/vishvananda/netlink"
@@ -234,7 +235,12 @@ func cmdAdd(args *skel.CmdArgs) error {
        }
 
        // run the IPAM plugin and get back the config to apply
-       result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+
+       result, err := current.GetResult(r)
        if err != nil {
                return err
        }
index a86a781..c7316bb 100644 (file)
@@ -25,6 +25,7 @@ import (
        "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/vishvananda/netlink"
 )
@@ -125,10 +126,15 @@ func cmdAdd(args *skel.CmdArgs) error {
        }
 
        // run the IPAM plugin and get back the config to apply
-       result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
        if err != nil {
                return err
        }
+       result, err := current.GetResult(r)
+       if err != nil {
+               return err
+       }
+
        if result.IP4 == nil {
                return errors.New("IPAM plugin returned missing IPv4 config")
        }
index 1344c13..d2b69f9 100644 (file)
@@ -17,7 +17,7 @@ package main
 import (
        "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/vishvananda/netlink"
 )
@@ -41,7 +41,7 @@ func cmdAdd(args *skel.CmdArgs) error {
                return err // not tested
        }
 
-       result := types.Result{}
+       result := current.Result{}
        return result.Print()
 }
 
index ef01269..8e2adeb 100644 (file)
@@ -25,6 +25,7 @@ import (
        "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/vishvananda/netlink"
@@ -141,10 +142,15 @@ func cmdAdd(args *skel.CmdArgs) error {
        }
 
        // run the IPAM plugin and get back the config to apply
-       result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+       r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
        if err != nil {
                return err
        }
+       result, err := current.GetResult(r)
+       if err != nil {
+               return err
+       }
+
        if result.IP4 == nil {
                return errors.New("IPAM plugin returned missing IPv4 config")
        }
index a26b09e..efda0be 100644 (file)
@@ -29,6 +29,7 @@ import (
        "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"
 )
@@ -46,7 +47,7 @@ type NetConf struct {
        MTU    int  `json:"mtu"`
 }
 
-func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string, error) {
+func setupContainerVeth(netns, ifName string, mtu int, pr *current.Result) (string, error) {
        // The IPAM result will be something like IP=192.168.3.5/24, GW=192.168.3.1.
        // What we want is really a point-to-point link but veth does not support IFF_POINTOPONT.
        // Next best thing would be to let it ARP but set interface to 192.168.3.5/32 and
@@ -102,7 +103,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string
                }
 
                for _, r := range []netlink.Route{
-                       netlink.Route{
+                       {
                                LinkIndex: contVeth.Attrs().Index,
                                Dst: &net.IPNet{
                                        IP:   pr.IP4.Gateway,
@@ -111,7 +112,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string
                                Scope: netlink.SCOPE_LINK,
                                Src:   pr.IP4.IP.IP,
                        },
-                       netlink.Route{
+                       {
                                LinkIndex: contVeth.Attrs().Index,
                                Dst: &net.IPNet{
                                        IP:   pr.IP4.IP.IP.Mask(pr.IP4.IP.Mask),
@@ -132,7 +133,7 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string
        return hostVethName, err
 }
 
-func setupHostVeth(vethName string, ipConf *types.IPConfig) error {
+func setupHostVeth(vethName string, ipConf *current.IPConfig) error {
        // hostVeth moved namespaces and may have a new ifindex
        veth, err := netlink.LinkByName(vethName)
        if err != nil {
@@ -172,7 +173,11 @@ func cmdAdd(args *skel.CmdArgs) error {
        }
 
        // run the IPAM plugin and get back the config to apply
-       result, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
+       r, err := ipam.ExecAdd(conf.IPAM.Type, args.StdinData)
+       if err != nil {
+               return err
+       }
+       result, err := current.GetResult(r)
        if err != nil {
                return err
        }
index 98e92ec..b1e7b0f 100644 (file)
@@ -27,6 +27,7 @@ import (
        "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"
 )
 
@@ -67,7 +68,7 @@ func cmdAdd(args *skel.CmdArgs) error {
                return err
        }
 
-       result := types.Result{}
+       result := current.Result{}
        return result.Print()
 }
 
index 89370ef..924bb9d 100644 (file)
@@ -31,14 +31,15 @@ import (
 
        "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"
        noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
 )
 
 type NetConf struct {
        types.NetConf
-       DebugFile  string        `json:"debugFile"`
-       PrevResult *types.Result `json:"prevResult,omitempty"`
+       DebugFile  string          `json:"debugFile"`
+       PrevResult *current.Result `json:"prevResult,omitempty"`
 }
 
 func loadConf(bytes []byte) (*NetConf, error) {
@@ -121,7 +122,12 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
                return errors.New(debug.ReportError)
        } else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" {
                if debug.ReportResult == "INJECT-DNS" {
-                       netConf.PrevResult.DNS.Nameservers = []string{"1.2.3.4"}
+                       newResult, err := current.NewResultFromResult(netConf.PrevResult)
+                       if err != nil {
+                               return err
+                       }
+                       newResult.DNS.Nameservers = []string{"1.2.3.4"}
+                       netConf.PrevResult = newResult
                }
                newResult, err := json.Marshal(netConf.PrevResult)
                if err != nil {
diff --git a/test b/test
index 673b08d..511997a 100755 (executable)
--- a/test
+++ b/test
@@ -11,7 +11,7 @@ set -e
 
 source ./build
 
-TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel"
+TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/ipam/host-local/backend/allocator plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/types/current pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip pkg/version pkg/version/testhelpers plugins/meta/flannel"
 FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning"
 
 # user has not provided PKG override