Further progress on versioning support (Issue #266).
Bump CNI spec version to 0.3.0
"os/exec"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
func pluginErr(err error, output []byte) error {
return err
}
+func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) {
+ stdoutBytes, err := execPlugin(pluginPath, nil, &Args{Command: "VERSION"})
+ if err != nil {
+ return nil, err
+ }
+
+ return version.Decode(stdoutBytes)
+}
+
func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) {
return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
}
}
type dispatcher struct {
- Getenv func(string) string
- Stdin io.Reader
- Stdout io.Writer
- Stderr io.Writer
- Versioner version.PluginVersioner
+ Getenv func(string) string
+ Stdin io.Reader
+ Stdout io.Writer
+ Stderr io.Writer
}
type reqForCmdEntry map[string]bool
}
}
-func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Error {
+func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) *types.Error {
cmd, cmdArgs, err := t.getCmdArgsFromEnv()
if err != nil {
return createTypedError(err.Error())
err = cmdDel(cmdArgs)
case "VERSION":
- err = t.Versioner.Encode(t.Stdout)
+ err = versionInfo.Encode(t.Stdout)
default:
return createTypedError("unknown CNI_COMMAND: %v", cmd)
// PluginMain is the "main" for a plugin. It accepts
// two callback functions for add and del commands.
-func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) {
+func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo) {
caller := dispatcher{
- Getenv: os.Getenv,
- Stdin: os.Stdin,
- Stdout: os.Stdout,
- Stderr: os.Stderr,
- Versioner: version.DefaultPluginVersioner,
+ Getenv: os.Getenv,
+ Stdin: os.Stdin,
+ Stdout: os.Stdout,
+ Stderr: os.Stderr,
}
- err := caller.pluginMain(cmdAdd, cmdDel)
+ err := caller.pluginMain(cmdAdd, cmdDel, versionInfo)
if err != nil {
dieErr(err)
}
cmdAdd, cmdDel *fakeCmd
dispatch *dispatcher
expectedCmdArgs *CmdArgs
+ versionInfo version.PluginInfo
)
BeforeEach(func() {
stdin = strings.NewReader(`{ "some": "config" }`)
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
- versioner := &version.BasicVersioner{CNIVersion: "9.8.7"}
+ versionInfo = version.PluginSupports("9.8.7")
dispatch = &dispatcher{
- Getenv: func(key string) string { return environment[key] },
- Stdin: stdin,
- Stdout: stdout,
- Stderr: stderr,
- Versioner: versioner,
+ Getenv: func(key string) string { return environment[key] },
+ Stdin: stdin,
+ Stdout: stdout,
+ Stderr: stderr,
}
cmdAdd = &fakeCmd{}
cmdDel = &fakeCmd{}
var envVarChecker = func(envVar string, isRequired bool) {
delete(environment, envVar)
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
if isRequired {
Expect(err).To(Equal(&types.Error{
Code: 100,
Context("when the CNI_COMMAND is ADD", func() {
It("extracts env vars and stdin data and calls cmdAdd", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(1))
})
It("does not call cmdDel", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdDel.CallCount).To(Equal(0))
})
It("reports that all of them are missing, not just the first", func() {
- Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)).NotTo(Succeed())
+ Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)).NotTo(Succeed())
log := stderr.String()
Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
})
It("calls cmdDel with the env vars and stdin data", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdDel.CallCount).To(Equal(1))
})
It("does not call cmdAdd", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
})
It("prints the version to stdout", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
- Expect(stdout).To(MatchJSON(`{ "cniVersion": "9.8.7" }`))
+ Expect(stdout).To(MatchJSON(`{
+ "cniVersion": "0.3.0",
+ "supportedVersions": ["9.8.7"]
+ }`))
})
It("does not call cmdAdd or cmdDel", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).NotTo(HaveOccurred())
Expect(cmdAdd.CallCount).To(Equal(0))
})
It("does not call any cmd callback", func() {
- dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0))
})
It("returns an error", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,
})
It("does not call any cmd callback", func() {
- dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(cmdAdd.CallCount).To(Equal(0))
Expect(cmdDel.CallCount).To(Equal(0))
})
It("wraps and returns the error", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,
})
It("returns the error as-is", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 1234,
})
It("wraps and returns the error", func() {
- err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)
+ err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
Expect(err).To(Equal(&types.Error{
Code: 100,
import (
"encoding/json"
+ "fmt"
"io"
)
-// A PluginVersioner can encode information about its version
-type PluginVersioner interface {
+// Current reports the version of the CNI spec implemented by this library
+func Current() string {
+ return "0.3.0"
+}
+
+// PluginInfo reports information about CNI versioning
+type PluginInfo interface {
+ // SupportedVersions returns one or more CNI spec versions that the plugin
+ // supports. If input is provided in one of these versions, then the plugin
+ // promises to use the same CNI version in its response
+ SupportedVersions() []string
+
+ // Encode writes this CNI version information as JSON to the given Writer
Encode(io.Writer) error
}
-// BasicVersioner is a PluginVersioner which reports a single cniVersion string
-type BasicVersioner struct {
- CNIVersion string `json:"cniVersion"`
+type simple struct {
+ CNIVersion_ string `json:"cniVersion"`
+ SupportedVersions_ []string `json:"supportedVersions,omitempty"`
}
-func (p *BasicVersioner) Encode(w io.Writer) error {
+func (p *simple) Encode(w io.Writer) error {
return json.NewEncoder(w).Encode(p)
}
-// Current reports the version of the CNI spec implemented by this library
-func Current() string {
- return "0.2.0"
+func (p *simple) SupportedVersions() []string {
+ return p.SupportedVersions_
+}
+
+// PluginSupports returns a new PluginInfo that will report the given versions
+// as supported
+func PluginSupports(supportedVersions ...string) PluginInfo {
+ if len(supportedVersions) < 1 {
+ panic("programmer error: you must support at least one version")
+ }
+ return &simple{
+ CNIVersion_: Current(),
+ SupportedVersions_: supportedVersions,
+ }
+}
+
+func Decode(jsonBytes []byte) (PluginInfo, error) {
+ var info simple
+ err := json.Unmarshal(jsonBytes, &info)
+ if err != nil {
+ return nil, fmt.Errorf("decoding version info: %s", err)
+ }
+ if info.CNIVersion_ == "" {
+ return nil, fmt.Errorf("decoding version info: missing field cniVersion")
+ }
+ if len(info.SupportedVersions_) == 0 {
+ if info.CNIVersion_ == "0.2.0" {
+ return PluginSupports("0.1.0", "0.2.0"), nil
+ }
+ return nil, fmt.Errorf("decoding version info: missing field supportedVersions")
+ }
+ return &info, nil
}
-// DefaultPluginVersioner reports the Current library spec version as the cniVersion
-var DefaultPluginVersioner = &BasicVersioner{CNIVersion: Current()}
+var Legacy = PluginSupports("0.1.0", "0.2.0", "0.3.0")
--- /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/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestVersion(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Version Suite")
+}
--- /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("Decode", func() {
+ It("returns a PluginInfo that represents the given json bytes", func() {
+ pluginInfo, err := version.Decode([]byte(`{
+ "cniVersion": "some-library-version",
+ "supportedVersions": [ "some-version", "some-other-version" ]
+ }`))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(pluginInfo).NotTo(BeNil())
+ Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
+ "some-version",
+ "some-other-version",
+ }))
+ })
+
+ Context("when the bytes cannot be decoded as json", func() {
+ It("returns a meaningful error", func() {
+ _, err := version.Decode([]byte(`{{{`))
+ Expect(err).To(MatchError("decoding version info: invalid character '{' looking for beginning of object key string"))
+ })
+ })
+
+ Context("when the json bytes are missing the required CNIVersion field", func() {
+ It("returns a meaningful error", func() {
+ _, err := version.Decode([]byte(`{ "supportedVersions": [ "foo" ] }`))
+ Expect(err).To(MatchError("decoding version info: missing field cniVersion"))
+ })
+ })
+
+ Context("when there are no supported versions", func() {
+ Context("when the cniVersion is 0.2.0", func() {
+ It("infers the supported versions are 0.1.0 and 0.2.0", func() {
+ pluginInfo, err := version.Decode([]byte(`{ "cniVersion": "0.2.0" }`))
+ Expect(err).NotTo(HaveOccurred())
+ Expect(pluginInfo).NotTo(BeNil())
+ Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
+ "0.1.0",
+ "0.2.0",
+ }))
+ })
+ })
+
+ Context("when the cniVersion is >= 0.3.0", func() {
+ It("returns a meaningful error", func() {
+ _, err := version.Decode([]byte(`{ "cniVersion": "0.3.0" }`))
+ Expect(err).To(MatchError("decoding version info: missing field supportedVersions"))
+ })
+ })
+ })
+
+})