github.com/mccv1r0/cni@v0.7.0-alpha1/pkg/skel/skel_test.go (about)

     1  // Copyright 2016 CNI authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package skel
    16  
    17  import (
    18  	"bytes"
    19  	"errors"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/containernetworking/cni/pkg/types"
    24  	"github.com/containernetworking/cni/pkg/types/current"
    25  	"github.com/containernetworking/cni/pkg/version"
    26  
    27  	. "github.com/onsi/ginkgo"
    28  	. "github.com/onsi/ginkgo/extensions/table"
    29  	. "github.com/onsi/gomega"
    30  )
    31  
    32  type fakeCmd struct {
    33  	CallCount int
    34  	Returns   struct {
    35  		Error error
    36  	}
    37  	Received struct {
    38  		CmdArgs *CmdArgs
    39  	}
    40  }
    41  
    42  func (c *fakeCmd) Func(args *CmdArgs) error {
    43  	c.CallCount++
    44  	c.Received.CmdArgs = args
    45  	return c.Returns.Error
    46  }
    47  
    48  var _ = Describe("dispatching to the correct callback", func() {
    49  	var (
    50  		environment            map[string]string
    51  		stdinData              string
    52  		stdout, stderr         *bytes.Buffer
    53  		cmdAdd, cmdGet, cmdDel *fakeCmd
    54  		dispatch               *dispatcher
    55  		expectedCmdArgs        *CmdArgs
    56  		versionInfo            version.PluginInfo
    57  	)
    58  
    59  	BeforeEach(func() {
    60  		environment = map[string]string{
    61  			"CNI_COMMAND":     "ADD",
    62  			"CNI_CONTAINERID": "some-container-id",
    63  			"CNI_NETNS":       "/some/netns/path",
    64  			"CNI_IFNAME":      "eth0",
    65  			"CNI_ARGS":        "some;extra;args",
    66  			"CNI_PATH":        "/some/cni/path",
    67  		}
    68  
    69  		stdinData = `{ "name":"skel-test", "some": "config", "cniVersion": "9.8.7" }`
    70  		stdout = &bytes.Buffer{}
    71  		stderr = &bytes.Buffer{}
    72  		versionInfo = version.PluginSupports("9.8.7")
    73  		dispatch = &dispatcher{
    74  			Getenv: func(key string) string { return environment[key] },
    75  			Stdin:  strings.NewReader(stdinData),
    76  			Stdout: stdout,
    77  			Stderr: stderr,
    78  		}
    79  		cmdAdd = &fakeCmd{}
    80  		cmdGet = &fakeCmd{}
    81  		cmdDel = &fakeCmd{}
    82  		expectedCmdArgs = &CmdArgs{
    83  			ContainerID: "some-container-id",
    84  			Netns:       "/some/netns/path",
    85  			IfName:      "eth0",
    86  			Args:        "some;extra;args",
    87  			Path:        "/some/cni/path",
    88  			StdinData:   []byte(stdinData),
    89  		}
    90  	})
    91  
    92  	var envVarChecker = func(envVar string, isRequired bool) {
    93  		delete(environment, envVar)
    94  
    95  		err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
    96  		if isRequired {
    97  			Expect(err).To(Equal(&types.Error{
    98  				Code: 100,
    99  				Msg:  "required env variables missing",
   100  			}))
   101  			Expect(stderr.String()).To(ContainSubstring(envVar + " env variable missing\n"))
   102  		} else {
   103  			Expect(err).NotTo(HaveOccurred())
   104  		}
   105  	}
   106  
   107  	Context("when the CNI_COMMAND is ADD", func() {
   108  		It("extracts env vars and stdin data and calls cmdAdd", func() {
   109  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   110  
   111  			Expect(err).NotTo(HaveOccurred())
   112  			Expect(cmdAdd.CallCount).To(Equal(1))
   113  			Expect(cmdGet.CallCount).To(Equal(0))
   114  			Expect(cmdDel.CallCount).To(Equal(0))
   115  			Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
   116  		})
   117  
   118  		It("does not call cmdGet or cmdDel", func() {
   119  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   120  
   121  			Expect(err).NotTo(HaveOccurred())
   122  			Expect(cmdGet.CallCount).To(Equal(0))
   123  			Expect(cmdDel.CallCount).To(Equal(0))
   124  		})
   125  
   126  		DescribeTable("required / optional env vars", envVarChecker,
   127  			Entry("command", "CNI_COMMAND", true),
   128  			Entry("container id", "CNI_CONTAINERID", true),
   129  			Entry("net ns", "CNI_NETNS", true),
   130  			Entry("if name", "CNI_IFNAME", true),
   131  			Entry("args", "CNI_ARGS", false),
   132  			Entry("path", "CNI_PATH", true),
   133  		)
   134  
   135  		Context("when multiple required env vars are missing", func() {
   136  			BeforeEach(func() {
   137  				delete(environment, "CNI_NETNS")
   138  				delete(environment, "CNI_IFNAME")
   139  				delete(environment, "CNI_PATH")
   140  			})
   141  
   142  			It("reports that all of them are missing, not just the first", func() {
   143  				Expect(dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")).NotTo(Succeed())
   144  
   145  				log := stderr.String()
   146  				Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
   147  				Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
   148  				Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
   149  
   150  			})
   151  		})
   152  
   153  		Context("when the stdin data is missing the required cniVersion config", func() {
   154  			BeforeEach(func() {
   155  				dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "some": "config" }`)
   156  			})
   157  
   158  			Context("when the plugin supports version 0.1.0", func() {
   159  				BeforeEach(func() {
   160  					versionInfo = version.PluginSupports("0.1.0")
   161  					expectedCmdArgs.StdinData = []byte(`{ "name": "skel-test", "some": "config" }`)
   162  				})
   163  
   164  				It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
   165  
   166  					err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   167  					Expect(err).NotTo(HaveOccurred())
   168  
   169  					Expect(cmdAdd.CallCount).To(Equal(1))
   170  					Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
   171  				})
   172  			})
   173  
   174  			Context("when the plugin does not support 0.1.0", func() {
   175  				BeforeEach(func() {
   176  					versionInfo = version.PluginSupports("4.3.2")
   177  				})
   178  
   179  				It("immediately returns a useful error", func() {
   180  					err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   181  					Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
   182  					Expect(err.Msg).To(Equal("incompatible CNI versions"))
   183  					Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`))
   184  				})
   185  
   186  				It("does not call either callback", func() {
   187  					dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   188  					Expect(cmdAdd.CallCount).To(Equal(0))
   189  					Expect(cmdGet.CallCount).To(Equal(0))
   190  					Expect(cmdDel.CallCount).To(Equal(0))
   191  				})
   192  			})
   193  		})
   194  	})
   195  
   196  	Context("when the CNI_COMMAND is GET", func() {
   197  		BeforeEach(func() {
   198  			environment["CNI_COMMAND"] = "GET"
   199  		})
   200  
   201  		It("extracts env vars and stdin data and calls cmdGet", func() {
   202  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   203  
   204  			Expect(err).NotTo(HaveOccurred())
   205  			Expect(cmdAdd.CallCount).To(Equal(0))
   206  			Expect(cmdGet.CallCount).To(Equal(1))
   207  			Expect(cmdDel.CallCount).To(Equal(0))
   208  			Expect(cmdGet.Received.CmdArgs).To(Equal(expectedCmdArgs))
   209  		})
   210  
   211  		It("does not call cmdAdd or cmdDel", func() {
   212  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   213  
   214  			Expect(err).NotTo(HaveOccurred())
   215  			Expect(cmdAdd.CallCount).To(Equal(0))
   216  			Expect(cmdDel.CallCount).To(Equal(0))
   217  		})
   218  
   219  		DescribeTable("required / optional env vars", envVarChecker,
   220  			Entry("command", "CNI_COMMAND", true),
   221  			Entry("container id", "CNI_CONTAINERID", true),
   222  			Entry("net ns", "CNI_NETNS", true),
   223  			Entry("if name", "CNI_IFNAME", true),
   224  			Entry("args", "CNI_ARGS", false),
   225  			Entry("path", "CNI_PATH", true),
   226  		)
   227  
   228  		Context("when multiple required env vars are missing", func() {
   229  			BeforeEach(func() {
   230  				delete(environment, "CNI_NETNS")
   231  				delete(environment, "CNI_IFNAME")
   232  				delete(environment, "CNI_PATH")
   233  			})
   234  
   235  			It("reports that all of them are missing, not just the first", func() {
   236  				Expect(dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")).NotTo(Succeed())
   237  				log := stderr.String()
   238  				Expect(log).To(ContainSubstring("CNI_NETNS env variable missing\n"))
   239  				Expect(log).To(ContainSubstring("CNI_IFNAME env variable missing\n"))
   240  				Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
   241  
   242  			})
   243  		})
   244  
   245  		Context("when cniVersion is less than 0.4.0", func() {
   246  			It("immediately returns a useful error", func() {
   247  				dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.3.0", "some": "config" }`)
   248  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   249  				Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
   250  				Expect(err.Msg).To(Equal("config version does not allow GET"))
   251  				Expect(cmdAdd.CallCount).To(Equal(0))
   252  				Expect(cmdGet.CallCount).To(Equal(0))
   253  				Expect(cmdDel.CallCount).To(Equal(0))
   254  			})
   255  		})
   256  
   257  		Context("when plugin does not support 0.4.0", func() {
   258  			It("immediately returns a useful error", func() {
   259  				dispatch.Stdin = strings.NewReader(`{ "name": "skel-test", "cniVersion": "0.4.0", "some": "config" }`)
   260  				versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
   261  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   262  				Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
   263  				Expect(err.Msg).To(Equal("plugin version does not allow GET"))
   264  				Expect(cmdAdd.CallCount).To(Equal(0))
   265  				Expect(cmdGet.CallCount).To(Equal(0))
   266  				Expect(cmdDel.CallCount).To(Equal(0))
   267  			})
   268  		})
   269  
   270  		Context("when the config has a bad version", func() {
   271  			It("immediately returns a useful error", func() {
   272  				dispatch.Stdin = strings.NewReader(`{ "cniVersion": "adsfsadf", "some": "config" }`)
   273  				versionInfo = version.PluginSupports("0.1.0", "0.2.0", "0.3.0")
   274  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   275  				Expect(err.Code).To(Equal(uint(100)))
   276  				Expect(cmdAdd.CallCount).To(Equal(0))
   277  				Expect(cmdGet.CallCount).To(Equal(0))
   278  				Expect(cmdDel.CallCount).To(Equal(0))
   279  			})
   280  		})
   281  
   282  		Context("when the plugin has a bad version", func() {
   283  			It("immediately returns a useful error", func() {
   284  				dispatch.Stdin = strings.NewReader(`{ "cniVersion": "0.4.0", "some": "config" }`)
   285  				versionInfo = version.PluginSupports("0.1.0", "0.2.0", "adsfasdf")
   286  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   287  				Expect(err.Code).To(Equal(uint(100)))
   288  				Expect(cmdAdd.CallCount).To(Equal(0))
   289  				Expect(cmdGet.CallCount).To(Equal(0))
   290  				Expect(cmdDel.CallCount).To(Equal(0))
   291  			})
   292  		})
   293  	})
   294  
   295  	Context("when the CNI_COMMAND is DEL", func() {
   296  		BeforeEach(func() {
   297  			environment["CNI_COMMAND"] = "DEL"
   298  		})
   299  
   300  		It("calls cmdDel with the env vars and stdin data", func() {
   301  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   302  
   303  			Expect(err).NotTo(HaveOccurred())
   304  			Expect(cmdDel.CallCount).To(Equal(1))
   305  			Expect(cmdDel.Received.CmdArgs).To(Equal(expectedCmdArgs))
   306  		})
   307  
   308  		It("does not call cmdAdd", func() {
   309  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   310  
   311  			Expect(err).NotTo(HaveOccurred())
   312  			Expect(cmdAdd.CallCount).To(Equal(0))
   313  		})
   314  
   315  		DescribeTable("required / optional env vars", envVarChecker,
   316  			Entry("command", "CNI_COMMAND", true),
   317  			Entry("container id", "CNI_CONTAINERID", true),
   318  			Entry("net ns", "CNI_NETNS", false),
   319  			Entry("if name", "CNI_IFNAME", true),
   320  			Entry("args", "CNI_ARGS", false),
   321  			Entry("path", "CNI_PATH", true),
   322  		)
   323  	})
   324  
   325  	Context("when the CNI_COMMAND is VERSION", func() {
   326  		BeforeEach(func() {
   327  			environment["CNI_COMMAND"] = "VERSION"
   328  		})
   329  
   330  		It("prints the version to stdout", func() {
   331  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   332  
   333  			Expect(err).NotTo(HaveOccurred())
   334  			Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
   335  				"cniVersion": "%s",
   336  				"supportedVersions": ["9.8.7"]
   337  			}`, current.ImplementedSpecVersion)))
   338  		})
   339  
   340  		It("does not call cmdAdd or cmdDel", func() {
   341  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   342  
   343  			Expect(err).NotTo(HaveOccurred())
   344  			Expect(cmdAdd.CallCount).To(Equal(0))
   345  			Expect(cmdDel.CallCount).To(Equal(0))
   346  		})
   347  
   348  		DescribeTable("VERSION does not need the usual env vars", envVarChecker,
   349  			Entry("command", "CNI_COMMAND", true),
   350  			Entry("container id", "CNI_CONTAINERID", false),
   351  			Entry("net ns", "CNI_NETNS", false),
   352  			Entry("if name", "CNI_IFNAME", false),
   353  			Entry("args", "CNI_ARGS", false),
   354  			Entry("path", "CNI_PATH", false),
   355  		)
   356  
   357  		It("does not read from Stdin", func() {
   358  			r := &BadReader{}
   359  			dispatch.Stdin = r
   360  
   361  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   362  
   363  			Expect(err).NotTo(HaveOccurred())
   364  			Expect(r.ReadCount).To(Equal(0))
   365  			Expect(stdout).To(MatchJSON(fmt.Sprintf(`{
   366  				"cniVersion": "%s",
   367  				"supportedVersions": ["9.8.7"]
   368  			}`, current.ImplementedSpecVersion)))
   369  		})
   370  	})
   371  
   372  	Context("when the CNI_COMMAND is unrecognized", func() {
   373  		BeforeEach(func() {
   374  			environment["CNI_COMMAND"] = "NOPE"
   375  		})
   376  
   377  		It("does not call any cmd callback", func() {
   378  			dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   379  
   380  			Expect(cmdAdd.CallCount).To(Equal(0))
   381  			Expect(cmdDel.CallCount).To(Equal(0))
   382  		})
   383  
   384  		It("returns an error", func() {
   385  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   386  
   387  			Expect(err).To(Equal(&types.Error{
   388  				Code: 100,
   389  				Msg:  "unknown CNI_COMMAND: NOPE",
   390  			}))
   391  		})
   392  
   393  		It("prints the about string when the command is blank", func() {
   394  			environment["CNI_COMMAND"] = ""
   395  			dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "test framework v42")
   396  			Expect(stderr.String()).To(ContainSubstring("test framework v42"))
   397  		})
   398  	})
   399  
   400  	Context("when stdin cannot be read", func() {
   401  		BeforeEach(func() {
   402  			dispatch.Stdin = &BadReader{}
   403  		})
   404  
   405  		It("does not call any cmd callback", func() {
   406  			dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   407  
   408  			Expect(cmdAdd.CallCount).To(Equal(0))
   409  			Expect(cmdDel.CallCount).To(Equal(0))
   410  		})
   411  
   412  		It("wraps and returns the error", func() {
   413  			err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   414  
   415  			Expect(err).To(Equal(&types.Error{
   416  				Code: 100,
   417  				Msg:  "error reading from stdin: banana",
   418  			}))
   419  		})
   420  	})
   421  
   422  	Context("when the callback returns an error", func() {
   423  		Context("when it is a typed Error", func() {
   424  			BeforeEach(func() {
   425  				cmdAdd.Returns.Error = &types.Error{
   426  					Code: 1234,
   427  					Msg:  "insufficient something",
   428  				}
   429  			})
   430  
   431  			It("returns the error as-is", func() {
   432  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   433  
   434  				Expect(err).To(Equal(&types.Error{
   435  					Code: 1234,
   436  					Msg:  "insufficient something",
   437  				}))
   438  			})
   439  		})
   440  
   441  		Context("when it is an unknown error", func() {
   442  			BeforeEach(func() {
   443  				cmdAdd.Returns.Error = errors.New("potato")
   444  			})
   445  
   446  			It("wraps and returns the error", func() {
   447  				err := dispatch.pluginMain(cmdAdd.Func, cmdGet.Func, cmdDel.Func, versionInfo, "")
   448  
   449  				Expect(err).To(Equal(&types.Error{
   450  					Code: 100,
   451  					Msg:  "potato",
   452  				}))
   453  			})
   454  		})
   455  	})
   456  })
   457  
   458  // BadReader is an io.Reader which always errors
   459  type BadReader struct {
   460  	Error     error
   461  	ReadCount int
   462  }
   463  
   464  func (r *BadReader) Read(buffer []byte) (int, error) {
   465  	r.ReadCount++
   466  	if r.Error != nil {
   467  		return 0, r.Error
   468  	}
   469  	return 0, errors.New("banana")
   470  }
   471  
   472  func (r *BadReader) Close() error {
   473  	return nil
   474  }