Further progress on versioning support (Issue #266).
Bump CNI spec version to 0.3.0
- Report version
- Parameters: NONE.
- - Result:
- - The version of the CNI spec implemented by the plugin: `{ "cniVersion": "0.2.0" }`
+ - Result: information about the CNI spec versions supported by the plugin
+
+ ```
+ {
+ "cniVersion": "0.3.0", // the version of the CNI spec in use for this output
+ "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0" ] // the list of CNI spec versions that this plugin supports
+ }
+ ```
The executable command-line API uses the type of network (see [Network Configuration](#network-configuration) below) as the name of the executable to invoke.
It will then look for this executable in a list of predefined directories. Once found, it will invoke the executable using the following environment variables for argument passing:
```
{
- "cniVersion": "0.2.0",
+ "cniVersion": "0.3.0",
"ip4": {
"ip": <ipv4-and-subnet-in-CIDR>,
"gateway": <ipv4-of-the-gateway>, (optional)
Errors are indicated by a non-zero return code and the following JSON being printed to stdout:
```
{
- "cniVersion": "0.2.0",
+ "cniVersion": "0.3.0",
"code": <numeric-error-code>,
"msg": <short-error-message>,
"details": <long-error-message> (optional)
```json
{
- "cniVersion": "0.2.0",
+ "cniVersion": "0.3.0",
"name": "dbnet",
"type": "bridge",
// type (plugin) specific
```json
{
- "cniVersion": "0.2.0",
+ "cniVersion": "0.3.0",
"name": "pci",
"type": "ovs",
// type (plugin) specific
```
{
- "cniVersion": "0.2.0",
+ "cniVersion": "0.3.0",
"ip4": {
"ip": <ipv4-and-subnet-in-CIDR>,
"gateway": <ipv4-of-the-gateway>, (optional)
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
type RuntimeConf struct {
return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt))
}
+func (c *CNIConfig) GetVersionInfo(pluginType string) (version.PluginInfo, error) {
+ pluginPath, err := invoke.FindInPath(pluginType, c.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO: if error is because plugin is old and VERSION command is unrecognized
+ // then do the right thing and return version.PluginSupports("0.1.0"), nil
+ return invoke.ExecPluginForVersion(pluginPath)
+}
+
// =====
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
return &invoke.Args{
})
})
})
+
+ Describe("GetVersionInfo", func() {
+ It("executes the plugin with the command VERSION", func() {
+ versionInfo, err := cniConfig.GetVersionInfo("noop")
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(versionInfo).NotTo(BeNil())
+ Expect(versionInfo.SupportedVersions()).To(Equal([]string{
+ "0.-42.0", "0.1.0", "0.2.0", "0.3.0",
+ }))
+ })
+
+ Context("when finding the plugin fails", func() {
+ It("returns the error", func() {
+ _, err := cniConfig.GetVersionInfo("does-not-exist")
+ Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`)))
+ })
+ })
+ })
})
"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"))
+ })
+ })
+ })
+
+})
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
const socketPath = "/run/cni/dhcp.sock"
if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon()
} else {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
}
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
func cmdAdd(args *skel.CmdArgs) error {
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
+ "github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils/sysctl"
+ "github.com/containernetworking/cni/pkg/version"
"github.com/vishvananda/netlink"
)
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/utils"
+ "github.com/containernetworking/cni/pkg/version"
)
func init() {
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
const (
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"github.com/containernetworking/cni/pkg/ns"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/version"
)
// TuningConf represents the network tuning configuration.
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
"strings"
"github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/plugins/test/noop/debug"
)
}
func main() {
- skel.PluginMain(cmdAdd, cmdDel)
+ skel.PluginMain(cmdAdd, cmdDel,
+ version.PluginSupports("0.-42.0", "0.1.0", "0.2.0", "0.3.0"))
}
source ./build
-TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local 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"
+TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local 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"
FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning"
# user has not provided PKG override