// 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 skel import ( "bytes" "errors" "io" "strings" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/version" "github.com/containernetworking/cni/pkg/testutils" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" ) type fakeCmd struct { CallCount int Returns struct { Error error } Received struct { CmdArgs *CmdArgs } } func (c *fakeCmd) Func(args *CmdArgs) error { c.CallCount++ c.Received.CmdArgs = args return c.Returns.Error } var _ = Describe("dispatching to the correct callback", func() { var ( environment map[string]string stdin io.Reader stdout, stderr *bytes.Buffer cmdAdd, cmdDel *fakeCmd dispatch *dispatcher expectedCmdArgs *CmdArgs ) BeforeEach(func() { environment = map[string]string{ "CNI_COMMAND": "ADD", "CNI_CONTAINERID": "some-container-id", "CNI_NETNS": "/some/netns/path", "CNI_IFNAME": "eth0", "CNI_ARGS": "some;extra;args", "CNI_PATH": "/some/cni/path", } stdin = strings.NewReader(`{ "some": "config" }`) stdout = &bytes.Buffer{} stderr = &bytes.Buffer{} versioner := &version.BasicVersioner{CNIVersion: "9.8.7"} dispatch = &dispatcher{ Getenv: func(key string) string { return environment[key] }, Stdin: stdin, Stdout: stdout, Stderr: stderr, Versioner: versioner, } cmdAdd = &fakeCmd{} cmdDel = &fakeCmd{} expectedCmdArgs = &CmdArgs{ ContainerID: "some-container-id", Netns: "/some/netns/path", IfName: "eth0", Args: "some;extra;args", Path: "/some/cni/path", StdinData: []byte(`{ "some": "config" }`), } }) var envVarChecker = func(envVar string, isRequired bool) { delete(environment, envVar) err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) if isRequired { Expect(err).To(Equal(&types.Error{ Code: 100, Msg: "required env variables missing", })) Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n")) } else { Expect(err).NotTo(HaveOccurred()) } } 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) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(1)) Expect(cmdDel.CallCount).To(Equal(0)) Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs)) }) It("does not call cmdDel", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).NotTo(HaveOccurred()) Expect(cmdDel.CallCount).To(Equal(0)) }) DescribeTable("required / optional env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), Entry("container id", "CNI_CONTAINER_ID", false), Entry("net ns", "CNI_NETNS", true), Entry("if name", "CNI_IFNAME", true), Entry("args", "CNI_ARGS", false), Entry("path", "CNI_PATH", true), ) Context("when multiple required env vars are missing", func() { BeforeEach(func() { delete(environment, "CNI_NETNS") delete(environment, "CNI_IFNAME") delete(environment, "CNI_PATH") }) It("reports that all of them are missing, not just the first", func() { Expect(dispatch.pluginMain(cmdAdd.Func, cmdDel.Func)).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")) Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n")) }) }) }) Context("when the CNI_COMMAND is DEL", func() { BeforeEach(func() { environment["CNI_COMMAND"] = "DEL" }) It("calls cmdDel with the env vars and stdin data", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).NotTo(HaveOccurred()) Expect(cmdDel.CallCount).To(Equal(1)) Expect(cmdDel.Received.CmdArgs).To(Equal(expectedCmdArgs)) }) It("does not call cmdAdd", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(0)) }) DescribeTable("required / optional env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), Entry("container id", "CNI_CONTAINER_ID", false), Entry("net ns", "CNI_NETNS", false), Entry("if name", "CNI_IFNAME", true), Entry("args", "CNI_ARGS", false), Entry("path", "CNI_PATH", true), ) }) Context("when the CNI_COMMAND is VERSION", func() { BeforeEach(func() { environment["CNI_COMMAND"] = "VERSION" }) It("prints the version to stdout", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).NotTo(HaveOccurred()) Expect(stdout).To(MatchJSON(`{ "cniVersion": "9.8.7" }`)) }) It("does not call cmdAdd or cmdDel", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).NotTo(HaveOccurred()) Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0)) }) DescribeTable("VERSION does not need the usual env vars", envVarChecker, Entry("command", "CNI_COMMAND", true), Entry("container id", "CNI_CONTAINER_ID", false), Entry("net ns", "CNI_NETNS", false), Entry("if name", "CNI_IFNAME", false), Entry("args", "CNI_ARGS", false), Entry("path", "CNI_PATH", false), ) }) Context("when the CNI_COMMAND is unrecognized", func() { BeforeEach(func() { environment["CNI_COMMAND"] = "NOPE" }) It("does not call any cmd callback", func() { dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(cmdAdd.CallCount).To(Equal(0)) Expect(cmdDel.CallCount).To(Equal(0)) }) It("returns an error", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).To(Equal(&types.Error{ Code: 100, Msg: "unknown CNI_COMMAND: NOPE", })) }) }) Context("when stdin cannot be read", func() { BeforeEach(func() { dispatch.Stdin = &testutils.BadReader{} }) It("does not call any cmd callback", func() { dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) 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) Expect(err).To(Equal(&types.Error{ Code: 100, Msg: "error reading from stdin: banana", })) }) }) Context("when the callback returns an error", func() { Context("when it is a typed Error", func() { BeforeEach(func() { cmdAdd.Returns.Error = &types.Error{ Code: 1234, Msg: "insufficient something", } }) It("returns the error as-is", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).To(Equal(&types.Error{ Code: 1234, Msg: "insufficient something", })) }) }) Context("when it is an unknown error", func() { BeforeEach(func() { cmdAdd.Returns.Error = errors.New("potato") }) It("wraps and returns the error", func() { err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func) Expect(err).To(Equal(&types.Error{ Code: 100, Msg: "potato", })) }) }) }) })