versioning: plugins require version match with config
infer version 0.1.0 when config is missing an explicit "cniVersion" field
This commit is contained in:
parent
1e4e8fdb5d
commit
04ba7c7761
33
skel/skel.go
33
skel/skel.go
@ -17,7 +17,6 @@
|
|||||||
package skel
|
package skel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -44,6 +43,9 @@ type dispatcher struct {
|
|||||||
Stdin io.Reader
|
Stdin io.Reader
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
|
||||||
|
ConfVersionDecoder version.ConfigDecoder
|
||||||
|
VersionReconciler version.Reconciler
|
||||||
}
|
}
|
||||||
|
|
||||||
type reqForCmdEntry map[string]bool
|
type reqForCmdEntry map[string]bool
|
||||||
@ -144,16 +146,20 @@ func createTypedError(f string, args ...interface{}) *types.Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *dispatcher) validateVersion(stdinData []byte) error {
|
func (t *dispatcher) checkVersionAndCall(cmdArgs *CmdArgs, pluginVersionInfo version.PluginInfo, toCall func(*CmdArgs) error) error {
|
||||||
var netconf types.NetConf
|
configVersion, err := t.ConfVersionDecoder.Decode(cmdArgs.StdinData)
|
||||||
if err := json.Unmarshal(stdinData, &netconf); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if netconf.CNIVersion == "" {
|
verErr := t.VersionReconciler.Check(configVersion, pluginVersionInfo)
|
||||||
return fmt.Errorf("missing required config cniVersion")
|
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 {
|
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())
|
return createTypedError(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = t.validateVersion(cmdArgs.StdinData); err != nil {
|
|
||||||
return createTypedError(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "ADD":
|
case "ADD":
|
||||||
err = cmdAdd(cmdArgs)
|
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
|
||||||
|
|
||||||
case "DEL":
|
case "DEL":
|
||||||
err = cmdDel(cmdArgs)
|
err = t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
|
||||||
|
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
err = versionInfo.Encode(t.Stdout)
|
err = versionInfo.Encode(t.Stdout)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return createTypedError("unknown CNI_COMMAND: %v", cmd)
|
return createTypedError("unknown CNI_COMMAND: %v", cmd)
|
||||||
}
|
}
|
||||||
|
@ -150,17 +150,41 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||||||
dispatch.Stdin = strings.NewReader(`{ "some": "config" }`)
|
dispatch.Stdin = strings.NewReader(`{ "some": "config" }`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Context("when the plugin supports version 0.1.0", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
versionInfo = version.PluginSupports("0.1.0")
|
||||||
|
expectedCmdArgs.StdinData = []byte(`{ "some": "config" }`)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
|
||||||
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(cmdAdd.CallCount).To(Equal(1))
|
||||||
|
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the plugin does not support 0.1.0", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
versionInfo = version.PluginSupports("4.3.2")
|
||||||
|
})
|
||||||
|
|
||||||
It("immediately returns a useful error", func() {
|
It("immediately returns a useful error", func() {
|
||||||
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||||
Expect(err).To(MatchError("missing required config cniVersion"))
|
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() {
|
It("does not call either callback", func() {
|
||||||
|
dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
||||||
Expect(cmdAdd.CallCount).To(Equal(0))
|
Expect(cmdAdd.CallCount).To(Equal(0))
|
||||||
Expect(cmdDel.CallCount).To(Equal(0))
|
Expect(cmdDel.CallCount).To(Equal(0))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Context("when the CNI_COMMAND is DEL", func() {
|
Context("when the CNI_COMMAND is DEL", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
@ -223,6 +247,22 @@ var _ = Describe("dispatching to the correct callback", func() {
|
|||||||
Entry("args", "CNI_ARGS", false),
|
Entry("args", "CNI_ARGS", false),
|
||||||
Entry("path", "CNI_PATH", 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() {
|
Context("when the CNI_COMMAND is unrecognized", func() {
|
||||||
|
@ -112,6 +112,14 @@ type Route struct {
|
|||||||
GW net.IP
|
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 {
|
type Error struct {
|
||||||
Code uint `json:"code"`
|
Code uint `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
|
@ -59,7 +59,7 @@ func PluginSupports(supportedVersions ...string) PluginInfo {
|
|||||||
// PluginDecoder can decode the response returned by a plugin's VERSION command
|
// PluginDecoder can decode the response returned by a plugin's VERSION command
|
||||||
type PluginDecoder struct{}
|
type PluginDecoder struct{}
|
||||||
|
|
||||||
func (_ *PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
|
||||||
var info pluginInfo
|
var info pluginInfo
|
||||||
err := json.Unmarshal(jsonBytes, &info)
|
err := json.Unmarshal(jsonBytes, &info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
47
version/reconcile.go
Normal file
47
version/reconcile.go
Normal file
@ -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,
|
||||||
|
}
|
||||||
|
}
|
51
version/reconcile_test.go
Normal file
51
version/reconcile_test.go
Normal file
@ -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"]`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user