versioning: plugins require version match with config
authorGabe Rosenhouse <grosenhouse@pivotal.io>
Mon, 19 Sep 2016 20:00:49 +0000 (13:00 -0700)
committerGabe Rosenhouse <grosenhouse@pivotal.io>
Mon, 19 Sep 2016 20:00:49 +0000 (13:00 -0700)
infer version 0.1.0 when config is missing an explicit "cniVersion" field

pkg/skel/skel.go
pkg/skel/skel_test.go
pkg/types/types.go
pkg/version/plugin.go
pkg/version/reconcile.go [new file with mode: 0644]
pkg/version/reconcile_test.go [new file with mode: 0644]

index 180ce24..8fc9636 100644 (file)
@@ -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)
        }
index 7ae25d3..570f027 100644 (file)
@@ -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() {
index ba02580..17caa49 100644 (file)
@@ -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"`
index 3a42fe2..dc937b5 100644 (file)
@@ -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/pkg/version/reconcile.go b/pkg/version/reconcile.go
new file mode 100644 (file)
index 0000000..f61ef65
--- /dev/null
@@ -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/pkg/version/reconcile_test.go b/pkg/version/reconcile_test.go
new file mode 100644 (file)
index 0000000..19a9e23
--- /dev/null
@@ -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"]`))
+               })
+       })
+})