Merge pull request #287 from rosenhouse/multi-version

Plugins report a list of supported versions
This commit is contained in:
Gabe Rosenhouse 2016-09-12 13:44:14 -07:00 committed by GitHub
commit 56032390fe
34 changed files with 1182 additions and 197 deletions

10
SPEC.md
View File

@ -64,8 +64,14 @@ The operations that the CNI plugin needs to support are:
- 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.2.0", // the version of the CNI spec in use for this output
"supportedVersions": [ "0.1.0", "0.2.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:

View File

@ -19,6 +19,7 @@ import (
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
type RuntimeConf struct {
@ -42,6 +43,7 @@ type CNIConfig struct {
Path []string
}
// AddNetwork executes the plugin with the ADD command
func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) {
pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
if err != nil {
@ -51,6 +53,7 @@ func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Resu
return invoke.ExecPluginWithResult(pluginPath, net.Bytes, c.args("ADD", rt))
}
// DelNetwork executes the plugin with the DEL command
func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error {
pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
if err != nil {
@ -60,6 +63,17 @@ func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error {
return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt))
}
// GetVersionInfo reports which versions of the CNI spec are supported by
// the given plugin.
func (c *CNIConfig) GetVersionInfo(pluginType string) (version.PluginInfo, error) {
pluginPath, err := invoke.FindInPath(pluginType, c.Path)
if err != nil {
return nil, err
}
return invoke.GetVersionInfo(pluginPath)
}
// =====
func (c *CNIConfig) args(action string, rt *RuntimeConf) *invoke.Args {
return &invoke.Args{

View File

@ -155,4 +155,23 @@ var _ = Describe("Invoking the plugin", func() {
})
})
})
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",
}))
})
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"`)))
})
})
})
})

View File

@ -15,34 +15,41 @@
package invoke
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
func pluginErr(err error, output []byte) error {
if _, ok := err.(*exec.ExitError); ok {
emsg := types.Error{}
if perr := json.Unmarshal(output, &emsg); perr != nil {
return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
}
details := ""
if emsg.Details != "" {
details = fmt.Sprintf("; %v", emsg.Details)
}
return fmt.Errorf("%v%v", emsg.Msg, details)
}
return err
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
return defaultPluginExec.WithResult(pluginPath, netconf, args)
}
func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
stdoutBytes, err := execPlugin(pluginPath, netconf, args)
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
}
func GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
return defaultPluginExec.GetVersionInfo(pluginPath)
}
var defaultPluginExec = &PluginExec{
RawExec: &RawExec{Stderr: os.Stderr},
VersionDecoder: &version.PluginDecoder{},
}
type PluginExec struct {
RawExec interface {
ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error)
}
VersionDecoder interface {
Decode(jsonBytes []byte) (version.PluginInfo, error)
}
}
func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
if err != nil {
return nil, err
}
@ -52,35 +59,31 @@ func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*typ
return res, err
}
func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
_, err := execPlugin(pluginPath, netconf, args)
func (e *PluginExec) WithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
_, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
return err
}
func execPlugin(pluginPath string, netconf []byte, args CNIArgs) ([]byte, error) {
return defaultRawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
}
// GetVersionInfo returns the version information available about the plugin.
// For recent-enough plugins, it uses the information returned by the VERSION
// command. For older plugins which do not recognize that command, it reports
// version 0.1.0
func (e *PluginExec) GetVersionInfo(pluginPath string) (version.PluginInfo, error) {
args := &Args{
Command: "VERSION",
var defaultRawExec = &RawExec{Stderr: os.Stderr}
type RawExec struct {
Stderr io.Writer
}
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{}
c := exec.Cmd{
Env: environ,
Path: pluginPath,
Args: []string{pluginPath},
Stdin: bytes.NewBuffer(stdinData),
Stdout: stdout,
Stderr: e.Stderr,
// set fake values required by plugins built against an older version of skel
NetNS: "dummy",
IfName: "dummy",
Path: "dummy",
}
if err := c.Run(); err != nil {
return nil, pluginErr(err, stdout.Bytes())
stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv())
if err != nil {
if err.Error() == "unknown CNI_COMMAND: VERSION" {
return version.PluginSupports("0.1.0"), nil
}
return nil, err
}
return stdout.Bytes(), nil
return e.VersionDecoder.Decode(stdoutBytes)
}

View File

@ -15,109 +15,137 @@
package invoke_test
import (
"bytes"
"io/ioutil"
"os"
"errors"
"github.com/containernetworking/cni/pkg/invoke"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
"github.com/containernetworking/cni/pkg/invoke/fakes"
"github.com/containernetworking/cni/pkg/version"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("RawExec", func() {
var _ = Describe("Executing a plugin, unit tests", func() {
var (
debugFileName string
debug *noop_debug.Debug
environ []string
stdin []byte
execer *invoke.RawExec
pluginExec *invoke.PluginExec
rawExec *fakes.RawExec
versionDecoder *fakes.VersionDecoder
pluginPath string
netconf []byte
cniargs *fakes.CNIArgs
)
const reportResult = `{ "some": "result" }`
BeforeEach(func() {
debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name()
rawExec = &fakes.RawExec{}
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ip4": { "ip": "1.2.3.4/24" } }`)
debug = &noop_debug.Debug{
ReportResult: reportResult,
ReportStderr: "some stderr message",
versionDecoder = &fakes.VersionDecoder{}
versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
pluginExec = &invoke.PluginExec{
RawExec: rawExec,
VersionDecoder: versionDecoder,
}
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
environ = []string{
"CNI_COMMAND=ADD",
"CNI_CONTAINERID=some-container-id",
"CNI_ARGS=DEBUG=" + debugFileName,
"CNI_NETNS=/some/netns/path",
"CNI_PATH=/some/bin/path",
"CNI_IFNAME=some-eth0",
}
stdin = []byte(`{"some":"stdin-json"}`)
execer = &invoke.RawExec{}
pluginPath = "/some/plugin/path"
netconf = []byte(`{ "some": "stdin" }`)
cniargs = &fakes.CNIArgs{}
cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
})
AfterEach(func() {
Expect(os.Remove(debugFileName)).To(Succeed())
})
It("runs the plugin with the given stdin and environment", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
debug, err := noop_debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD"))
Expect(debug.CmdArgs.StdinData).To(Equal(stdin))
Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path"))
})
It("returns the resulting stdout as bytes", func() {
resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
Expect(resultBytes).To(BeEquivalentTo(reportResult))
})
Context("when the Stderr writer is set", func() {
var stderrBuffer *bytes.Buffer
BeforeEach(func() {
stderrBuffer = &bytes.Buffer{}
execer.Stderr = stderrBuffer
})
It("forwards any stderr bytes to the Stderr writer", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Describe("returning a result", func() {
It("unmarshals the result bytes into the Result type", func() {
result, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(err).NotTo(HaveOccurred())
Expect(result.IP4.IP.IP.String()).To(Equal("1.2.3.4"))
})
Expect(stderrBuffer.String()).To(Equal("some stderr message"))
It("passes its arguments through to the rawExec", func() {
pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
_, err := pluginExec.WithResult(pluginPath, netconf, cniargs)
Expect(err).To(MatchError("banana"))
})
})
})
Context("when the plugin errors", func() {
Describe("without returning a result", func() {
It("passes its arguments through to the rawExec", func() {
pluginExec.WithoutResult(pluginPath, netconf, cniargs)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(Equal(netconf))
Expect(rawExec.ExecPluginCall.Received.Environ).To(Equal([]string{"SOME=ENV"}))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
err := pluginExec.WithoutResult(pluginPath, netconf, cniargs)
Expect(err).To(MatchError("banana"))
})
})
})
Describe("discovering the plugin version", func() {
BeforeEach(func() {
debug.ReportError = "banana"
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`)
})
It("wraps and returns the error", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("banana"))
It("execs the plugin with the command VERSION", func() {
pluginExec.GetVersionInfo(pluginPath)
Expect(rawExec.ExecPluginCall.Received.PluginPath).To(Equal(pluginPath))
Expect(rawExec.ExecPluginCall.Received.StdinData).To(BeNil())
Expect(rawExec.ExecPluginCall.Received.Environ).To(ContainElement("CNI_COMMAND=VERSION"))
})
})
Context("when the system is unable to execute the plugin", func() {
It("returns the error", func() {
_, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path")))
It("decodes and returns the version info", func() {
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(Equal([]string{"0.42.0"}))
Expect(versionDecoder.DecodeCall.Received.JSONBytes).To(MatchJSON(`{ "some": "version-info" }`))
})
Context("when the rawExec fails", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
})
It("returns the error", func() {
_, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).To(MatchError("banana"))
})
})
Context("when the plugin is too old to recognize the VERSION command", func() {
BeforeEach(func() {
rawExec.ExecPluginCall.Returns.Error = errors.New("unknown CNI_COMMAND: VERSION")
})
It("interprets the error as a 0.1.0 version", func() {
versionInfo, err := pluginExec.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(ConsistOf("0.1.0"))
})
It("sets dummy values for env vars required by very old plugins", func() {
pluginExec.GetVersionInfo(pluginPath)
env := rawExec.ExecPluginCall.Received.Environ
Expect(env).To(ContainElement("CNI_NETNS=dummy"))
Expect(env).To(ContainElement("CNI_IFNAME=dummy"))
Expect(env).To(ContainElement("CNI_PATH=dummy"))
})
})
})
})

View File

@ -0,0 +1,27 @@
// 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 fakes
type CNIArgs struct {
AsEnvCall struct {
Returns struct {
Env []string
}
}
}
func (a *CNIArgs) AsEnv() []string {
return a.AsEnvCall.Returns.Env
}

View File

@ -0,0 +1,36 @@
// 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 fakes
type RawExec struct {
ExecPluginCall struct {
Received struct {
PluginPath string
StdinData []byte
Environ []string
}
Returns struct {
ResultBytes []byte
Error error
}
}
}
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
e.ExecPluginCall.Received.PluginPath = pluginPath
e.ExecPluginCall.Received.StdinData = stdinData
e.ExecPluginCall.Received.Environ = environ
return e.ExecPluginCall.Returns.ResultBytes, e.ExecPluginCall.Returns.Error
}

View File

@ -0,0 +1,34 @@
// 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 fakes
import "github.com/containernetworking/cni/pkg/version"
type VersionDecoder struct {
DecodeCall struct {
Received struct {
JSONBytes []byte
}
Returns struct {
PluginInfo version.PluginInfo
Error error
}
}
}
func (e *VersionDecoder) Decode(jsonData []byte) (version.PluginInfo, error) {
e.DecodeCall.Received.JSONBytes = jsonData
return e.DecodeCall.Returns.PluginInfo, e.DecodeCall.Returns.Error
}

View File

@ -0,0 +1,107 @@
// 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 invoke_test
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/pkg/version/testhelpers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)
var _ = Describe("GetVersion, integration tests", func() {
var (
pluginDir string
pluginPath string
)
BeforeEach(func() {
pluginDir, err := ioutil.TempDir("", "plugins")
Expect(err).NotTo(HaveOccurred())
pluginPath = filepath.Join(pluginDir, "test-plugin")
})
AfterEach(func() {
Expect(os.RemoveAll(pluginDir)).To(Succeed())
})
DescribeTable("correctly reporting plugin versions",
func(gitRef string, pluginSource string, expectedVersions version.PluginInfo) {
Expect(testhelpers.BuildAt([]byte(pluginSource), gitRef, pluginPath)).To(Succeed())
versionInfo, err := invoke.GetVersionInfo(pluginPath)
Expect(err).NotTo(HaveOccurred())
Expect(versionInfo.SupportedVersions()).To(ConsistOf(expectedVersions.SupportedVersions()))
},
Entry("historical: before VERSION was introduced",
git_ref_v010, plugin_source_no_custom_versions,
version.PluginSupports("0.1.0"),
),
Entry("historical: when VERSION was introduced but plugins couldn't customize it",
git_ref_v020_no_custom_versions, plugin_source_no_custom_versions,
version.PluginSupports("0.1.0", "0.2.0"),
),
Entry("historical: when plugins started reporting their own version list",
git_ref_v020_custom_versions, plugin_source_v020_custom_versions,
version.PluginSupports("0.2.0", "0.999.0"),
),
// this entry tracks the current behavior. Before you change it, ensure
// that its previous behavior is captured in the most recent "historical" entry
Entry("current",
"HEAD", plugin_source_v020_custom_versions,
version.PluginSupports("0.2.0", "0.999.0"),
),
)
})
// a 0.2.0 plugin that can report its own versions
const plugin_source_v020_custom_versions = `package main
import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
"fmt"
)
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
func main() { skel.PluginMain(c, c, version.PluginSupports("0.2.0", "0.999.0")) }
`
const git_ref_v020_custom_versions = "bf31ed15"
// a minimal 0.1.0 / 0.2.0 plugin that cannot report it's own version support
const plugin_source_no_custom_versions = `package main
import "github.com/containernetworking/cni/pkg/skel"
import "fmt"
func c(_ *skel.CmdArgs) error { fmt.Println("{}"); return nil }
func main() { skel.PluginMain(c, c) }
`
const git_ref_v010 = "2c482f4"
const git_ref_v020_no_custom_versions = "349d66d"

63
pkg/invoke/raw_exec.go Normal file
View File

@ -0,0 +1,63 @@
// 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 invoke
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os/exec"
"github.com/containernetworking/cni/pkg/types"
)
type RawExec struct {
Stderr io.Writer
}
func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
stdout := &bytes.Buffer{}
c := exec.Cmd{
Env: environ,
Path: pluginPath,
Args: []string{pluginPath},
Stdin: bytes.NewBuffer(stdinData),
Stdout: stdout,
Stderr: e.Stderr,
}
if err := c.Run(); err != nil {
return nil, pluginErr(err, stdout.Bytes())
}
return stdout.Bytes(), nil
}
func pluginErr(err error, output []byte) error {
if _, ok := err.(*exec.ExitError); ok {
emsg := types.Error{}
if perr := json.Unmarshal(output, &emsg); perr != nil {
return fmt.Errorf("netplugin failed but error parsing its diagnostic message %q: %v", string(output), perr)
}
details := ""
if emsg.Details != "" {
details = fmt.Sprintf("; %v", emsg.Details)
}
return fmt.Errorf("%v%v", emsg.Msg, details)
}
return err
}

123
pkg/invoke/raw_exec_test.go Normal file
View File

@ -0,0 +1,123 @@
// 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 invoke_test
import (
"bytes"
"io/ioutil"
"os"
"github.com/containernetworking/cni/pkg/invoke"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("RawExec", func() {
var (
debugFileName string
debug *noop_debug.Debug
environ []string
stdin []byte
execer *invoke.RawExec
)
const reportResult = `{ "some": "result" }`
BeforeEach(func() {
debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed())
debugFileName = debugFile.Name()
debug = &noop_debug.Debug{
ReportResult: reportResult,
ReportStderr: "some stderr message",
}
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
environ = []string{
"CNI_COMMAND=ADD",
"CNI_CONTAINERID=some-container-id",
"CNI_ARGS=DEBUG=" + debugFileName,
"CNI_NETNS=/some/netns/path",
"CNI_PATH=/some/bin/path",
"CNI_IFNAME=some-eth0",
}
stdin = []byte(`{"some":"stdin-json"}`)
execer = &invoke.RawExec{}
})
AfterEach(func() {
Expect(os.Remove(debugFileName)).To(Succeed())
})
It("runs the plugin with the given stdin and environment", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
debug, err := noop_debug.ReadDebug(debugFileName)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD"))
Expect(debug.CmdArgs.StdinData).To(Equal(stdin))
Expect(debug.CmdArgs.Netns).To(Equal("/some/netns/path"))
})
It("returns the resulting stdout as bytes", func() {
resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
Expect(resultBytes).To(BeEquivalentTo(reportResult))
})
Context("when the Stderr writer is set", func() {
var stderrBuffer *bytes.Buffer
BeforeEach(func() {
stderrBuffer = &bytes.Buffer{}
execer.Stderr = stderrBuffer
})
It("forwards any stderr bytes to the Stderr writer", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).NotTo(HaveOccurred())
Expect(stderrBuffer.String()).To(Equal("some stderr message"))
})
})
Context("when the plugin errors", func() {
BeforeEach(func() {
debug.ReportError = "banana"
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("wraps and returns the error", func() {
_, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("banana"))
})
})
Context("when the system is unable to execute the plugin", func() {
It("returns the error", func() {
_, err := execer.ExecPlugin("/tmp/some/invalid/plugin/path", stdin, environ)
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("/tmp/some/invalid/plugin/path")))
})
})
})

View File

@ -39,11 +39,10 @@ type CmdArgs struct {
}
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
@ -144,7 +143,7 @@ func createTypedError(f string, args ...interface{}) *types.Error {
}
}
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())
@ -158,7 +157,7 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er
err = cmdDel(cmdArgs)
case "VERSION":
err = t.Versioner.Encode(t.Stdout)
err = versionInfo.Encode(t.Stdout)
default:
return createTypedError("unknown CNI_COMMAND: %v", cmd)
@ -176,16 +175,15 @@ func (t *dispatcher) pluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) *types.Er
// 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)
}

View File

@ -53,6 +53,7 @@ var _ = Describe("dispatching to the correct callback", func() {
cmdAdd, cmdDel *fakeCmd
dispatch *dispatcher
expectedCmdArgs *CmdArgs
versionInfo version.PluginInfo
)
BeforeEach(func() {
@ -67,13 +68,12 @@ var _ = Describe("dispatching to the correct callback", 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{}
@ -90,7 +90,7 @@ var _ = Describe("dispatching to the correct callback", func() {
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,
@ -104,7 +104,7 @@ var _ = Describe("dispatching to the correct callback", func() {
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))
@ -113,7 +113,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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))
@ -136,7 +136,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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"))
@ -152,7 +152,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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))
@ -160,7 +160,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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))
@ -182,14 +182,17 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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.2.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))
@ -212,14 +215,14 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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,
@ -234,14 +237,14 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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,
@ -260,7 +263,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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,
@ -275,7 +278,7 @@ var _ = Describe("dispatching to the correct callback", func() {
})
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,

View File

@ -57,6 +57,8 @@ func (n *IPNet) UnmarshalJSON(data []byte) error {
// NetConf describes a network.
type NetConf struct {
CNIVersion string `json:"cniVersion,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
IPAM struct {

77
pkg/version/plugin.go Normal file
View File

@ -0,0 +1,77 @@
// 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 (
"encoding/json"
"fmt"
"io"
)
// 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
}
type pluginInfo struct {
CNIVersion_ string `json:"cniVersion"`
SupportedVersions_ []string `json:"supportedVersions,omitempty"`
}
func (p *pluginInfo) Encode(w io.Writer) error {
return json.NewEncoder(w).Encode(p)
}
func (p *pluginInfo) 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 &pluginInfo{
CNIVersion_: Current(),
SupportedVersions_: supportedVersions,
}
}
type PluginDecoder struct{}
func (_ *PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) {
var info pluginInfo
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
}

View File

@ -0,0 +1,85 @@
// 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("Decoding versions reported by a plugin", func() {
var (
decoder *version.PluginDecoder
versionStdout []byte
)
BeforeEach(func() {
decoder = &version.PluginDecoder{}
versionStdout = []byte(`{
"cniVersion": "some-library-version",
"supportedVersions": [ "some-version", "some-other-version" ]
}`)
})
It("returns a PluginInfo that represents the given json bytes", func() {
pluginInfo, err := decoder.Decode(versionStdout)
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() {
BeforeEach(func() {
versionStdout = []byte(`{{{`)
})
It("returns a meaningful error", func() {
_, err := decoder.Decode(versionStdout)
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() {
BeforeEach(func() {
versionStdout = []byte(`{ "supportedVersions": [ "foo" ] }`)
})
It("returns a meaningful error", func() {
_, err := decoder.Decode(versionStdout)
Expect(err).To(MatchError("decoding version info: missing field cniVersion"))
})
})
Context("when there are no supported versions", func() {
BeforeEach(func() {
versionStdout = []byte(`{ "cniVersion": "0.2.0" }`)
})
It("assumes that the supported versions are 0.1.0 and 0.2.0", func() {
pluginInfo, err := decoder.Decode(versionStdout)
Expect(err).NotTo(HaveOccurred())
Expect(pluginInfo).NotTo(BeNil())
Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
"0.1.0",
"0.2.0",
}))
})
})
})

View File

@ -0,0 +1,156 @@
// 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 testhelpers supports testing of CNI components of different versions
//
// For example, to build a plugin against an old version of the CNI library,
// we can pass the plugin's source and the old git commit reference to BuildAt.
// We could then test how the built binary responds when called by the latest
// version of this library.
package testhelpers
import (
"fmt"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
const packageBaseName = "github.com/containernetworking/cni"
func run(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput()
if err != nil {
command := strings.Join(cmd.Args, " ")
return fmt.Errorf("running %q: %s", command, out)
}
return nil
}
func goBuildEnviron(gopath string) []string {
environ := os.Environ()
for i, kvp := range environ {
if strings.HasPrefix(kvp, "GOPATH=") {
environ[i] = "GOPATH=" + gopath
return environ
}
}
environ = append(environ, "GOPATH="+gopath)
return environ
}
func buildGoProgram(gopath, packageName, outputFilePath string) error {
cmd := exec.Command("go", "build", "-o", outputFilePath, packageName)
cmd.Env = goBuildEnviron(gopath)
return run(cmd)
}
func createSingleFilePackage(gopath, packageName string, fileContents []byte) error {
dirName := filepath.Join(gopath, "src", packageName)
err := os.MkdirAll(dirName, 0700)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(dirName, "main.go"), fileContents, 0600)
}
func removePackage(gopath, packageName string) error {
dirName := filepath.Join(gopath, "src", packageName)
return os.RemoveAll(dirName)
}
func isRepoRoot(path string) bool {
_, err := ioutil.ReadDir(filepath.Join(path, ".git"))
return (err == nil) && (filepath.Base(path) == "cni")
}
func LocateCurrentGitRepo() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for i := 0; i < 5; i++ {
if isRepoRoot(dir) {
return dir, nil
}
dir, err = filepath.Abs(filepath.Dir(dir))
if err != nil {
return "", fmt.Errorf("abs(dir(%q)): %s", dir, err)
}
}
return "", fmt.Errorf("unable to find cni repo root, landed at %q", dir)
}
func gitCloneThisRepo(cloneDestination string) error {
err := os.MkdirAll(cloneDestination, 0700)
if err != nil {
return err
}
currentGitRepo, err := LocateCurrentGitRepo()
if err != nil {
return err
}
return run(exec.Command("git", "clone", currentGitRepo, cloneDestination))
}
func gitCheckout(localRepo string, gitRef string) error {
return run(exec.Command("git", "-C", localRepo, "checkout", gitRef))
}
// BuildAt builds the go programSource using the version of the CNI library
// at gitRef, and saves the resulting binary file at outputFilePath
func BuildAt(programSource []byte, gitRef string, outputFilePath string) error {
tempGoPath, err := ioutil.TempDir("", "cni-git-")
if err != nil {
return err
}
defer os.RemoveAll(tempGoPath)
cloneDestination := filepath.Join(tempGoPath, "src", packageBaseName)
err = gitCloneThisRepo(cloneDestination)
if err != nil {
return err
}
err = gitCheckout(cloneDestination, gitRef)
if err != nil {
return err
}
rand.Seed(time.Now().UnixNano())
testPackageName := fmt.Sprintf("test-package-%x", rand.Int31())
err = createSingleFilePackage(tempGoPath, testPackageName, programSource)
if err != nil {
return err
}
defer removePackage(tempGoPath, testPackageName)
err = buildGoProgram(tempGoPath, testPackageName, outputFilePath)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,27 @@
// 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 testhelpers_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestTesthelpers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Testhelpers Suite")
}

View File

@ -0,0 +1,106 @@
// 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 testhelpers_test
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"github.com/containernetworking/cni/pkg/version/testhelpers"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("BuildAt", func() {
var (
gitRef string
outputFilePath string
outputDir string
programSource []byte
)
BeforeEach(func() {
programSource = []byte(`package main
import "github.com/containernetworking/cni/pkg/skel"
func c(_ *skel.CmdArgs) error { return nil }
func main() { skel.PluginMain(c, c) }
`)
gitRef = "f4364185253"
var err error
outputDir, err = ioutil.TempDir("", "bin")
Expect(err).NotTo(HaveOccurred())
outputFilePath = filepath.Join(outputDir, "some-binary")
})
AfterEach(func() {
Expect(os.RemoveAll(outputDir)).To(Succeed())
})
It("builds the provided source code using the CNI library at the given git ref", func() {
Expect(outputFilePath).NotTo(BeAnExistingFile())
err := testhelpers.BuildAt(programSource, gitRef, outputFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(outputFilePath).To(BeAnExistingFile())
cmd := exec.Command(outputFilePath)
cmd.Env = []string{"CNI_COMMAND=VERSION"}
output, err := cmd.CombinedOutput()
Expect(err).To(BeAssignableToTypeOf(&exec.ExitError{}))
Expect(output).To(ContainSubstring("unknown CNI_COMMAND: VERSION"))
})
})
var _ = Describe("LocateCurrentGitRepo", func() {
It("returns the path to the root of the CNI git repo", func() {
path, err := testhelpers.LocateCurrentGitRepo()
Expect(err).NotTo(HaveOccurred())
AssertItIsTheCNIRepoRoot(path)
})
Context("when run from a different directory", func() {
BeforeEach(func() {
os.Chdir("..")
})
It("still finds the CNI repo root", func() {
path, err := testhelpers.LocateCurrentGitRepo()
Expect(err).NotTo(HaveOccurred())
AssertItIsTheCNIRepoRoot(path)
})
})
})
func AssertItIsTheCNIRepoRoot(path string) {
Expect(path).To(BeADirectory())
files, err := ioutil.ReadDir(path)
Expect(err).NotTo(HaveOccurred())
names := []string{}
for _, file := range files {
names = append(names, file.Name())
}
Expect(names).To(ContainElement("SPEC.md"))
Expect(names).To(ContainElement("libcni"))
Expect(names).To(ContainElement("cnitool"))
}

View File

@ -14,29 +14,16 @@
package version
import (
"encoding/json"
"io"
)
// A PluginVersioner can encode information about its version
type PluginVersioner interface {
Encode(io.Writer) error
}
// BasicVersioner is a PluginVersioner which reports a single cniVersion string
type BasicVersioner struct {
CNIVersion string `json:"cniVersion"`
}
func (p *BasicVersioner) 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"
}
// DefaultPluginVersioner reports the Current library spec version as the cniVersion
var DefaultPluginVersioner = &BasicVersioner{CNIVersion: Current()}
// Legacy PluginInfo describes a plugin that is backwards compatible with the
// CNI spec version 0.1.0. In particular, a runtime compiled against the 0.1.0
// library ought to work correctly with a plugin that reports support for
// Legacy versions.
//
// Any future CNI spec versions which meet this definition should be added to
// this list.
var Legacy = PluginSupports("0.1.0", "0.2.0")

View File

@ -0,0 +1,27 @@
// 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")
}

View File

@ -22,6 +22,7 @@ import (
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/version"
)
const socketPath = "/run/cni/dhcp.sock"
@ -30,7 +31,7 @@ func main() {
if len(os.Args) > 1 && os.Args[1] == "daemon" {
runDaemon()
} else {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}
}

View File

@ -19,10 +19,11 @@ import (
"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 {

View File

@ -28,6 +28,7 @@ import (
"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"
)
@ -354,5 +355,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -25,6 +25,7 @@ import (
"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"
)
@ -171,5 +172,5 @@ func renameLink(curName, newName string) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -18,6 +18,7 @@ import (
"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"
)
@ -67,5 +68,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -26,6 +26,7 @@ import (
"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"
)
@ -193,5 +194,5 @@ func renameLink(curName, newName string) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -30,6 +30,7 @@ import (
"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() {
@ -236,5 +237,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -32,6 +32,7 @@ import (
"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 (
@ -249,5 +250,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -27,6 +27,7 @@ import (
"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.
@ -78,5 +79,5 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
skel.PluginMain(cmdAdd, cmdDel, version.Legacy)
}

View File

@ -24,11 +24,17 @@ import (
// Debug is used to control and record the behavior of the noop plugin
type Debug struct {
ReportResult string
ReportError string
ReportStderr string
Command string
CmdArgs skel.CmdArgs
// Report* fields allow the test to control the behavior of the no-op plugin
ReportResult string
ReportError string
ReportStderr string
ReportVersionSupport []string
// Command stores the CNI command that the plugin received
Command string
// CmdArgs stores the CNI Args and Env Vars that the plugin recieved
CmdArgs skel.CmdArgs
}
// ReadDebug will return a debug file recorded by the noop plugin

View File

@ -28,6 +28,7 @@ import (
"strings"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/cni/plugins/test/noop/debug"
)
@ -62,6 +63,23 @@ func debugBehavior(args *skel.CmdArgs, command string) error {
return nil
}
func debugGetSupportedVersions() []string {
vers := []string{"0.-42.0", "0.1.0", "0.2.0"}
cniArgs := os.Getenv("CNI_ARGS")
if cniArgs == "" {
return vers
}
debugFilePath := strings.TrimPrefix(cniArgs, "DEBUG=")
debug, err := debug.ReadDebug(debugFilePath)
if err != nil {
panic("test setup error: unable to read debug file: " + err.Error())
}
if debug.ReportVersionSupport == nil {
return vers
}
return debug.ReportVersionSupport
}
func cmdAdd(args *skel.CmdArgs) error {
return debugBehavior(args, "ADD")
}
@ -71,5 +89,6 @@ func cmdDel(args *skel.CmdArgs) error {
}
func main() {
skel.PluginMain(cmdAdd, cmdDel)
supportedVersions := debugGetSupportedVersions()
skel.PluginMain(cmdAdd, cmdDel, version.PluginSupports(supportedVersions...))
}

View File

@ -21,6 +21,7 @@ import (
"strings"
"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/version"
noop_debug "github.com/containernetworking/cni/plugins/test/noop/debug"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
@ -38,7 +39,10 @@ var _ = Describe("No-op plugin", func() {
const reportResult = `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }`
BeforeEach(func() {
debug = &noop_debug.Debug{ReportResult: reportResult}
debug = &noop_debug.Debug{
ReportResult: reportResult,
ReportVersionSupport: []string{"0.1.0", "0.2.0", "0.3.0"},
}
debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
@ -122,6 +126,25 @@ var _ = Describe("No-op plugin", func() {
Expect(debug.Command).To(Equal("DEL"))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
})
})
Context("when the CNI_COMMAND is VERSION", func() {
BeforeEach(func() {
cmd.Env[0] = "CNI_COMMAND=VERSION"
debug.ReportVersionSupport = []string{"0.123.0", "0.2.0"}
Expect(debug.WriteDebug(debugFileName)).To(Succeed())
})
It("claims to support the specified versions", func() {
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Eventually(session).Should(gexec.Exit(0))
decoder := &version.PluginDecoder{}
pluginInfo, err := decoder.Decode(session.Out.Contents())
Expect(err).NotTo(HaveOccurred())
Expect(pluginInfo.SupportedVersions()).To(ConsistOf(
"0.123.0", "0.2.0"))
})
})
})

2
test
View File

@ -11,7 +11,7 @@ set -e
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 pkg/version/testhelpers"
FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning"
# user has not provided PKG override