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)
- }
+func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
+ return defaultPluginExec.WithResult(pluginPath, netconf, args)
+}
- return err
+func ExecPluginWithoutResult(pluginPath string, netconf []byte, args CNIArgs) error {
+ return defaultPluginExec.WithoutResult(pluginPath, netconf, args)
}
-func ExecPluginWithResult(pluginPath string, netconf []byte, args CNIArgs) (*types.Result, error) {
- stdoutBytes, err := execPlugin(pluginPath, netconf, args)
+func ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) {
+ return defaultPluginExec.GetVersion(pluginPath)
+}
+
+var defaultPluginExec = &PluginExec{
+ RawExec: &RawExec{Stderr: os.Stderr},
+ VersionDecoder: &version.Decoder{},
+}
+
+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
}
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 ExecPluginForVersion(pluginPath string) (version.PluginInfo, error) {
- stdoutBytes, err := execPlugin(pluginPath, nil, &Args{Command: "VERSION"})
+func (e *PluginExec) GetVersion(pluginPath string) (version.PluginInfo, error) {
+ args := &Args{Command: "VERSION"}
+ stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, nil, args.AsEnv())
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())
-}
-
-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,
- }
- if err := c.Run(); err != nil {
- return nil, pluginErr(err, stdout.Bytes())
- }
-
- return stdout.Bytes(), nil
+ return e.VersionDecoder.Decode(stdoutBytes)
}
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", func() {
var (
- debugFileName string
- debug *noop_debug.Debug
- environ []string
- stdin []byte
- execer *invoke.RawExec
- )
+ pluginExec *invoke.PluginExec
+ rawExec *fakes.RawExec
+ versionDecoder *fakes.VersionDecoder
- const reportResult = `{ "some": "result" }`
+ pluginPath string
+ netconf []byte
+ cniargs *fakes.CNIArgs
+ )
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{}
- })
+ rawExec = &fakes.RawExec{}
+ rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ip4": { "ip": "1.2.3.4/24" } }`)
- 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())
+ versionDecoder = &fakes.VersionDecoder{}
+ versionDecoder.DecodeCall.Returns.PluginInfo = version.PluginSupports("0.42.0")
- 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"))
+ pluginExec = &invoke.PluginExec{
+ RawExec: rawExec,
+ VersionDecoder: versionDecoder,
+ }
+ pluginPath = "/some/plugin/path"
+ netconf = []byte(`{ "some": "stdin" }`)
+ cniargs = &fakes.CNIArgs{}
+ cniargs.AsEnvCall.Returns.Env = []string{"SOME=ENV"}
})
- It("returns the resulting stdout as bytes", func() {
- resultBytes, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
- Expect(err).NotTo(HaveOccurred())
-
- Expect(resultBytes).To(BeEquivalentTo(reportResult))
- })
+ 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"))
+ })
- Context("when the Stderr writer is set", func() {
- var stderrBuffer *bytes.Buffer
+ 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"}))
+ })
- BeforeEach(func() {
- stderrBuffer = &bytes.Buffer{}
- execer.Stderr = stderrBuffer
+ 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"))
+ })
})
+ })
- It("forwards any stderr bytes to the Stderr writer", func() {
- _, err := execer.ExecPlugin(pathToPlugin, stdin, environ)
- Expect(err).NotTo(HaveOccurred())
+ 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"}))
+ })
- Expect(stderrBuffer.String()).To(Equal("some stderr message"))
+ 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"))
+ })
})
})
- Context("when the plugin errors", func() {
+ 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.GetVersion(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"))
+ })
+
+ It("decodes and returns the version info", func() {
+ versionInfo, err := pluginExec.GetVersion(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 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")))
+ Context("when the rawExec fails", func() {
+ BeforeEach(func() {
+ rawExec.ExecPluginCall.Returns.Error = errors.New("banana")
+ })
+ It("returns the error", func() {
+ _, err := pluginExec.GetVersion(pluginPath)
+ Expect(err).To(MatchError("banana"))
+ })
})
})
})
--- /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 fakes
+
+type CNIArgs struct {
+ AsEnvCall struct {
+ Returns struct {
+ Env []string
+ }
+ }
+}
+
+func (a *CNIArgs) AsEnv() []string {
+ return a.AsEnvCall.Returns.Env
+}
--- /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 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
+}
--- /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 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
+}
--- /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 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
+}
--- /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 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")))
+ })
+ })
+})
}
}
-func Decode(jsonBytes []byte) (PluginInfo, error) {
+type Decoder struct{}
+
+func (_ *Decoder) Decode(jsonBytes []byte) (PluginInfo, error) {
var info simple
err := json.Unmarshal(jsonBytes, &info)
if err != nil {
)
var _ = Describe("Decode", func() {
+ var decoder *version.Decoder
+
+ BeforeEach(func() {
+ decoder = &version.Decoder{}
+ })
+
It("returns a PluginInfo that represents the given json bytes", func() {
- pluginInfo, err := version.Decode([]byte(`{
+ pluginInfo, err := decoder.Decode([]byte(`{
"cniVersion": "some-library-version",
"supportedVersions": [ "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(`{{{`))
+ _, err := decoder.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" ] }`))
+ _, err := decoder.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" }`))
+ pluginInfo, err := decoder.Decode([]byte(`{ "cniVersion": "0.2.0" }`))
Expect(err).NotTo(HaveOccurred())
Expect(pluginInfo).NotTo(BeNil())
Expect(pluginInfo.SupportedVersions()).To(Equal([]string{
Context("when the cniVersion is >= 0.3.0", func() {
It("returns a meaningful error", func() {
- _, err := version.Decode([]byte(`{ "cniVersion": "0.3.0" }`))
+ _, err := decoder.Decode([]byte(`{ "cniVersion": "0.3.0" }`))
Expect(err).To(MatchError("decoding version info: missing field supportedVersions"))
})
})