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 }