package skel
import (
- "encoding/json"
"fmt"
"io"
"io/ioutil"
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
+
+ ConfVersionDecoder version.ConfigDecoder
+ VersionReconciler version.Reconciler
}
type reqForCmdEntry map[string]bool
}
}
-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 {
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)
}
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))
+ })
})
})
})
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() {
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"`
// 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 {
--- /dev/null
+// 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,
+ }
+}
--- /dev/null
+// 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"]`))
+ })
+ })
+})