
Updates the spec and plugins to return an array of interfaces and IP details to the runtime including: - interface names and MAC addresses configured by the plugin - whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc) - multiple IP addresses configured by IPAM and which interface they have been assigned to Returning interface details is useful for runtimes, as well as allowing more flexible chaining of CNI plugins themselves. For example, some meta plugins may need to know the host-side interface to be able to apply firewall or traffic shaping rules to the container.
347 lines
9.7 KiB
Go
347 lines
9.7 KiB
Go
// 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"
|
|
"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
|
|
stdinData string
|
|
stdout, stderr *bytes.Buffer
|
|
cmdAdd, cmdDel *fakeCmd
|
|
dispatch *dispatcher
|
|
expectedCmdArgs *CmdArgs
|
|
versionInfo version.PluginInfo
|
|
)
|
|
|
|
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",
|
|
}
|
|
|
|
stdinData = `{ "some": "config", "cniVersion": "9.8.7" }`
|
|
stdout = &bytes.Buffer{}
|
|
stderr = &bytes.Buffer{}
|
|
versionInfo = version.PluginSupports("9.8.7")
|
|
dispatch = &dispatcher{
|
|
Getenv: func(key string) string { return environment[key] },
|
|
Stdin: strings.NewReader(stdinData),
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
}
|
|
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(stdinData),
|
|
}
|
|
})
|
|
|
|
var envVarChecker = func(envVar string, isRequired bool) {
|
|
delete(environment, envVar)
|
|
|
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
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, versionInfo)
|
|
|
|
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, versionInfo)
|
|
|
|
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_CONTAINERID", 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, 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"))
|
|
Expect(log).To(ContainSubstring("CNI_PATH env variable missing\n"))
|
|
|
|
})
|
|
})
|
|
|
|
Context("when the stdin data is missing the required cniVersion config", func() {
|
|
BeforeEach(func() {
|
|
dispatch.Stdin = strings.NewReader(`{ "some": "config" }`)
|
|
})
|
|
|
|
Context("when the plugin supports version 0.1.0", func() {
|
|
BeforeEach(func() {
|
|
versionInfo = version.PluginSupports("0.1.0")
|
|
expectedCmdArgs.StdinData = []byte(`{ "some": "config" }`)
|
|
})
|
|
|
|
It("infers the config is 0.1.0 and calls the cmdAdd callback", func() {
|
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(cmdAdd.CallCount).To(Equal(1))
|
|
Expect(cmdAdd.Received.CmdArgs).To(Equal(expectedCmdArgs))
|
|
})
|
|
})
|
|
|
|
Context("when the plugin does not support 0.1.0", func() {
|
|
BeforeEach(func() {
|
|
versionInfo = version.PluginSupports("4.3.2")
|
|
})
|
|
|
|
It("immediately returns a useful error", func() {
|
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
Expect(err.Code).To(Equal(types.ErrIncompatibleCNIVersion)) // see https://github.com/containernetworking/cni/blob/master/SPEC.md#well-known-error-codes
|
|
Expect(err.Msg).To(Equal("incompatible CNI versions"))
|
|
Expect(err.Details).To(Equal(`config is "0.1.0", plugin supports ["4.3.2"]`))
|
|
})
|
|
|
|
It("does not call either callback", func() {
|
|
dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
Expect(cmdAdd.CallCount).To(Equal(0))
|
|
Expect(cmdDel.CallCount).To(Equal(0))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
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, versionInfo)
|
|
|
|
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, versionInfo)
|
|
|
|
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_CONTAINERID", 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, versionInfo)
|
|
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(stdout).To(MatchJSON(`{
|
|
"cniVersion": "0.3.0",
|
|
"supportedVersions": ["9.8.7"]
|
|
}`))
|
|
})
|
|
|
|
It("does not call cmdAdd or cmdDel", func() {
|
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
|
|
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_CONTAINERID", 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 stdin is empty", func() {
|
|
BeforeEach(func() {
|
|
dispatch.Stdin = strings.NewReader("")
|
|
})
|
|
|
|
It("succeeds without error", func() {
|
|
err := dispatch.pluginMain(cmdAdd.Func, cmdDel.Func, versionInfo)
|
|
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(stdout).To(MatchJSON(`{
|
|
"cniVersion": "0.3.0",
|
|
"supportedVersions": ["9.8.7"]
|
|
}`))
|
|
})
|
|
})
|
|
})
|
|
|
|
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, 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, versionInfo)
|
|
|
|
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, 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, versionInfo)
|
|
|
|
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, versionInfo)
|
|
|
|
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, versionInfo)
|
|
|
|
Expect(err).To(Equal(&types.Error{
|
|
Code: 100,
|
|
Msg: "potato",
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
})
|