NetNS string
IfName string
Args [][2]string
+ // A dictionary of capability-specific data passed by the runtime
+ // to plugins as top-level keys in the 'runtimeConfig' dictionary
+ // of the plugin's stdin data. libcni will ensure that only keys
+ // in this map which match the capabilities of the plugin are passed
+ // to the plugin
+ CapabilityArgs map[string]interface{}
}
type NetworkConfig struct {
// CNIConfig implements the CNI interface
var _ CNI = &CNIConfig{}
-func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult types.Result) (*NetworkConfig, error) {
+func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (*NetworkConfig, error) {
var err error
+ inject := map[string]interface{}{
+ "name": list.Name,
+ "cniVersion": list.CNIVersion,
+ }
+ // Add previous plugin result
+ if prevResult != nil {
+ inject["prevResult"] = prevResult
+ }
+
// Ensure every config uses the same name and version
- orig, err = InjectConf(orig, "name", list.Name)
+ orig, err = InjectConf(orig, inject)
if err != nil {
return nil, err
}
- orig, err = InjectConf(orig, "cniVersion", list.CNIVersion)
- if err != nil {
- return nil, err
+
+ return injectRuntimeConfig(orig, rt)
+}
+
+// This function takes a libcni RuntimeConf structure and injects values into
+// a "runtimeConfig" dictionary in the CNI network configuration JSON that
+// will be passed to the plugin on stdin.
+//
+// Only "capabilities arguments" passed by the runtime are currently injected.
+// These capabilities arguments are filtered through the plugin's advertised
+// capabilities from its config JSON, and any keys in the CapabilityArgs
+// matching plugin capabilities are added to the "runtimeConfig" dictionary
+// sent to the plugin via JSON on stdin. For exmaple, if the plugin's
+// capabilities include "portMappings", and the CapabilityArgs map includes a
+// "portMappings" key, that key and its value are added to the "runtimeConfig"
+// dictionary to be passed to the plugin's stdin.
+func injectRuntimeConfig(orig *NetworkConfig, rt *RuntimeConf) (*NetworkConfig, error) {
+ var err error
+
+ rc := make(map[string]interface{})
+ for capability, supported := range orig.Network.Capabilities {
+ if !supported {
+ continue
+ }
+ if data, ok := rt.CapabilityArgs[capability]; ok {
+ rc[capability] = data
+ }
}
- // Add previous plugin result
- if prevResult != nil {
- orig, err = InjectConf(orig, "prevResult", prevResult)
+ if len(rc) > 0 {
+ orig, err = InjectConf(orig, map[string]interface{}{"runtimeConfig": rc})
if err != nil {
return nil, err
}
return nil, err
}
- newConf, err := buildOneConfig(list, net, prevResult)
+ newConf, err := buildOneConfig(list, net, prevResult, rt)
if err != nil {
return nil, err
}
return err
}
- newConf, err := buildOneConfig(list, net, nil)
+ newConf, err := buildOneConfig(list, net, nil, rt)
if err != nil {
return err
}
return nil, err
}
+ net, err = injectRuntimeConfig(net, rt)
+ if err != nil {
+ return nil, err
+ }
+
return invoke.ExecPluginWithResult(pluginPath, net.Bytes, c.args("ADD", rt))
}
return err
}
+ net, err = injectRuntimeConfig(net, rt)
+ if err != nil {
+ return err
+ }
+
return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt))
}
"fmt"
"io/ioutil"
"net"
+ "os"
"path/filepath"
"github.com/containernetworking/cni/libcni"
debugFilePath string
debug *noop_debug.Debug
config string
+ stdinData []byte
}
-func addNameToConfig(name, config string) ([]byte, error) {
- obj := make(map[string]interface{})
- err := json.Unmarshal([]byte(config), &obj)
- if err != nil {
- return nil, fmt.Errorf("unmarshal existing network bytes: %s", err)
+type portMapping struct {
+ HostPort int `json:"hostPort"`
+ ContainerPort int `json:"containerPort"`
+ Protocol string `json:"protocol"`
+}
+
+func stringInList(s string, list []string) bool {
+ for _, item := range list {
+ if s == item {
+ return true
+ }
}
- obj["name"] = name
- return json.Marshal(obj)
+ return false
}
-func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePath bool, result string) pluginInfo {
+func newPluginInfo(configValue, prevResult string, injectDebugFilePath bool, result string, runtimeConfig map[string]interface{}, capabilities []string) pluginInfo {
debugFile, err := ioutil.TempFile("", "cni_debug")
Expect(err).NotTo(HaveOccurred())
Expect(debugFile.Close()).To(Succeed())
}
Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
- config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.3.0"`, configKey, configValue)
+ // config is what would be in the plugin's on-disk configuration
+ // without runtime injected keys
+ config := fmt.Sprintf(`{"type": "noop", "some-key": "%s"`, configValue)
if prevResult != "" {
config += fmt.Sprintf(`, "prevResult": %s`, prevResult)
}
if injectDebugFilePath {
config += fmt.Sprintf(`, "debugFile": "%s"`, debugFilePath)
}
+ if len(capabilities) > 0 {
+ config += `, "capabilities": {`
+ for i, c := range capabilities {
+ if i > 0 {
+ config += ", "
+ }
+ config += fmt.Sprintf(`"%s": true`, c)
+ }
+ config += "}"
+ }
config += "}"
+ // stdinData is what the runtime should pass to the plugin's stdin,
+ // including injected keys like 'name', 'cniVersion', and 'runtimeConfig'
+ newConfig := make(map[string]interface{})
+ err = json.Unmarshal([]byte(config), &newConfig)
+ Expect(err).NotTo(HaveOccurred())
+ newConfig["name"] = "some-list"
+ newConfig["cniVersion"] = "0.3.0"
+
+ // Only include standard runtime config and capability args that this plugin advertises
+ newRuntimeConfig := make(map[string]interface{})
+ for key, value := range runtimeConfig {
+ if stringInList(key, capabilities) {
+ newRuntimeConfig[key] = value
+ }
+ }
+ if len(newRuntimeConfig) > 0 {
+ newConfig["runtimeConfig"] = newRuntimeConfig
+ }
+
+ stdinData, err := json.Marshal(newConfig)
+ Expect(err).NotTo(HaveOccurred())
+
return pluginInfo{
debugFilePath: debugFilePath,
debug: debug,
config: config,
+ stdinData: stdinData,
}
}
var _ = Describe("Invoking plugins", func() {
+ Describe("Capabilities", func() {
+ var (
+ debugFilePath string
+ debug *noop_debug.Debug
+ pluginConfig []byte
+ cniConfig libcni.CNIConfig
+ runtimeConfig *libcni.RuntimeConf
+ netConfig *libcni.NetworkConfig
+ )
+
+ BeforeEach(func() {
+ debugFile, err := ioutil.TempFile("", "cni_debug")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(debugFile.Close()).To(Succeed())
+ debugFilePath = debugFile.Name()
+
+ debug = &noop_debug.Debug{}
+ Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
+
+ pluginConfig = []byte(`{ "type": "noop", "cniVersion": "0.3.0", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`)
+ netConfig, err = libcni.ConfFromBytes(pluginConfig)
+ Expect(err).NotTo(HaveOccurred())
+
+ cniConfig = libcni.CNIConfig{Path: []string{filepath.Dir(pluginPaths["noop"])}}
+
+ runtimeConfig = &libcni.RuntimeConf{
+ ContainerID: "some-container-id",
+ NetNS: "/some/netns/path",
+ IfName: "some-eth0",
+ Args: [][2]string{{"DEBUG", debugFilePath}},
+ CapabilityArgs: map[string]interface{}{
+ "portMappings": []portMapping{
+ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
+ },
+ "somethingElse": []string{"foobar", "baz"},
+ "noCapability": true,
+ "notAdded": []bool{true, false},
+ },
+ }
+ })
+
+ AfterEach(func() {
+ Expect(os.RemoveAll(debugFilePath)).To(Succeed())
+ })
+
+ It("adds correct runtime config for capabilities to stdin", func() {
+ _, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
+ Expect(err).NotTo(HaveOccurred())
+
+ debug, err = noop_debug.ReadDebug(debugFilePath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(debug.Command).To(Equal("ADD"))
+
+ conf := make(map[string]interface{})
+ err = json.Unmarshal(debug.CmdArgs.StdinData, &conf)
+ Expect(err).NotTo(HaveOccurred())
+
+ // We expect runtimeConfig keys only for portMappings and somethingElse
+ rawRc := conf["runtimeConfig"]
+ rc, ok := rawRc.(map[string]interface{})
+ Expect(ok).To(Equal(true))
+ expectedKeys := []string{"portMappings", "somethingElse"}
+ Expect(len(rc)).To(Equal(len(expectedKeys)))
+ for _, key := range expectedKeys {
+ _, ok := rc[key]
+ Expect(ok).To(Equal(true))
+ }
+ })
+
+ It("adds no runtimeConfig when the plugin advertises no used capabilities", func() {
+ // Replace CapabilityArgs with ones we know the plugin
+ // doesn't support
+ runtimeConfig.CapabilityArgs = map[string]interface{}{
+ "portMappings22": []portMapping{
+ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
+ },
+ "somethingElse22": []string{"foobar", "baz"},
+ }
+
+ _, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
+ Expect(err).NotTo(HaveOccurred())
+
+ debug, err = noop_debug.ReadDebug(debugFilePath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(debug.Command).To(Equal("ADD"))
+
+ conf := make(map[string]interface{})
+ err = json.Unmarshal(debug.CmdArgs.StdinData, &conf)
+ Expect(err).NotTo(HaveOccurred())
+
+ // No intersection of plugin capabilities and CapabilityArgs,
+ // so plugin should not receive a "runtimeConfig" key
+ _, ok := conf["runtimeConfig"]
+ Expect(ok).Should(BeFalse())
+ })
+ })
+
Describe("Invoking a single plugin", func() {
var (
debugFilePath string
}
Expect(debug.WriteDebug(debugFilePath)).To(Succeed())
+ portMappings := []portMapping{
+ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
+ }
+
cniBinPath = filepath.Dir(pluginPaths["noop"])
- pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.3.0" }`
+ pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.3.0", "capabilities": { "portMappings": true } }`
cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
netConfig = &libcni.NetworkConfig{
Network: &types.NetConf{
Type: "noop",
+ Capabilities: map[string]bool{
+ "portMappings": true,
+ },
},
Bytes: []byte(pluginConfig),
}
ContainerID: "some-container-id",
NetNS: "/some/netns/path",
IfName: "some-eth0",
- Args: [][2]string{[2]string{"DEBUG", debugFilePath}},
+ Args: [][2]string{{"DEBUG", debugFilePath}},
+ CapabilityArgs: map[string]interface{}{
+ "portMappings": portMappings,
+ },
}
+ // inject runtime args into the expected plugin config
+ conf := make(map[string]interface{})
+ err = json.Unmarshal([]byte(pluginConfig), &conf)
+ Expect(err).NotTo(HaveOccurred())
+ conf["runtimeConfig"] = map[string]interface{}{
+ "portMappings": portMappings,
+ }
+ newBytes, err := json.Marshal(conf)
+ Expect(err).NotTo(HaveOccurred())
+
expectedCmdArgs = skel.CmdArgs{
ContainerID: "some-container-id",
Netns: "/some/netns/path",
IfName: "some-eth0",
Args: "DEBUG=" + debugFilePath,
Path: cniBinPath,
- StdinData: []byte(pluginConfig),
+ StdinData: newBytes,
}
})
+ AfterEach(func() {
+ Expect(os.RemoveAll(debugFilePath)).To(Succeed())
+ })
+
Describe("AddNetwork", func() {
It("executes the plugin with command ADD", func() {
r, err := cniConfig.AddNetwork(netConfig, runtimeConfig)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD"))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
+ Expect(string(debug.CmdArgs.StdinData)).To(ContainSubstring("\"portMappings\":"))
})
Context("when finding the plugin fails", func() {
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("DEL"))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
+ Expect(string(debug.CmdArgs.StdinData)).To(ContainSubstring("\"portMappings\":"))
})
Context("when finding the plugin fails", func() {
)
BeforeEach(func() {
+ var err error
+
+ capabilityArgs := map[string]interface{}{
+ "portMappings": []portMapping{
+ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp"},
+ },
+ "otherCapability": 33,
+ }
+
+ cniBinPath = filepath.Dir(pluginPaths["noop"])
+ cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
+ runtimeConfig = &libcni.RuntimeConf{
+ ContainerID: "some-container-id",
+ NetNS: "/some/netns/path",
+ IfName: "some-eth0",
+ Args: [][2]string{{"FOO", "BAR"}},
+ CapabilityArgs: capabilityArgs,
+ }
+
+ expectedCmdArgs = skel.CmdArgs{
+ ContainerID: runtimeConfig.ContainerID,
+ Netns: runtimeConfig.NetNS,
+ IfName: runtimeConfig.IfName,
+ Args: "FOO=BAR",
+ Path: cniBinPath,
+ }
+
+ rc := map[string]interface{}{
+ "containerId": runtimeConfig.ContainerID,
+ "netNs": runtimeConfig.NetNS,
+ "ifName": runtimeConfig.IfName,
+ "args": map[string]string{
+ "FOO": "BAR",
+ },
+ "portMappings": capabilityArgs["portMappings"],
+ "otherCapability": capabilityArgs["otherCapability"],
+ }
+
+ ipResult := `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`
plugins = make([]pluginInfo, 3, 3)
- plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`)
- plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "PASSTHROUGH")
- plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ips":[{"version": "4", "address": "10.1.2.3/24"}]}`, true, "INJECT-DNS")
+ plugins[0] = newPluginInfo("some-value", "", true, ipResult, rc, []string{"portMappings", "otherCapability"})
+ plugins[1] = newPluginInfo("some-other-value", ipResult, true, "PASSTHROUGH", rc, []string{"otherCapability"})
+ plugins[2] = newPluginInfo("yet-another-value", ipResult, true, "INJECT-DNS", rc, []string{})
configList := []byte(fmt.Sprintf(`{
"name": "some-list",
]
}`, plugins[0].config, plugins[1].config, plugins[2].config))
- var err error
netConfigList, err = libcni.ConfListFromBytes(configList)
Expect(err).NotTo(HaveOccurred())
+ })
- cniBinPath = filepath.Dir(pluginPaths["noop"])
- cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}}
- runtimeConfig = &libcni.RuntimeConf{
- ContainerID: "some-container-id",
- NetNS: "/some/netns/path",
- IfName: "some-eth0",
- Args: [][2]string{{"FOO", "BAR"}},
- }
-
- expectedCmdArgs = skel.CmdArgs{
- ContainerID: "some-container-id",
- Netns: "/some/netns/path",
- IfName: "some-eth0",
- Args: "FOO=BAR",
- Path: cniBinPath,
+ AfterEach(func() {
+ for _, p := range plugins {
+ Expect(os.RemoveAll(p.debugFilePath)).To(Succeed())
}
})
debug, err := noop_debug.ReadDebug(plugins[i].debugFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("ADD"))
- newConfig, err := addNameToConfig("some-list", plugins[i].config)
- Expect(err).NotTo(HaveOccurred())
// Must explicitly match JSON due to dict element ordering
- debugJSON := debug.CmdArgs.StdinData
+ Expect(debug.CmdArgs.StdinData).To(MatchJSON(plugins[i].stdinData))
debug.CmdArgs.StdinData = nil
- Expect(debugJSON).To(MatchJSON(newConfig))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
}
})
debug, err := noop_debug.ReadDebug(plugins[i].debugFilePath)
Expect(err).NotTo(HaveOccurred())
Expect(debug.Command).To(Equal("DEL"))
- newConfig, err := addNameToConfig("some-list", plugins[i].config)
- Expect(err).NotTo(HaveOccurred())
// Must explicitly match JSON due to dict element ordering
- debugJSON := debug.CmdArgs.StdinData
+ Expect(debug.CmdArgs.StdinData).To(MatchJSON(plugins[i].stdinData))
debug.CmdArgs.StdinData = nil
- Expect(debugJSON).To(MatchJSON(newConfig))
Expect(debug.CmdArgs).To(Equal(expectedCmdArgs))
}
})
})
})
+ Describe("Capabilities", func() {
+ var configDir string
+
+ BeforeEach(func() {
+ var err error
+ configDir, err = ioutil.TempDir("", "plugin-conf")
+ Expect(err).NotTo(HaveOccurred())
+
+ pluginConfig := []byte(`{ "name": "some-plugin", "type": "noop", "cniVersion": "0.3.0", "capabilities": { "portMappings": true, "somethingElse": true, "noCapability": false } }`)
+ Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed())
+ })
+
+ AfterEach(func() {
+ Expect(os.RemoveAll(configDir)).To(Succeed())
+ })
+
+ It("reads plugin capabilities from network config", func() {
+ netConfig, err := libcni.LoadConf(configDir, "some-plugin")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(netConfig.Network.Capabilities).To(Equal(map[string]bool{
+ "portMappings": true,
+ "somethingElse": true,
+ "noCapability": false,
+ }))
+ })
+ })
+
Describe("ConfFromFile", func() {
Context("when the file cannot be opened", func() {
It("returns a useful error", func() {
conf := &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"},
Bytes: []byte(`{ cc cc cc}`)}
- _, err := libcni.InjectConf(conf, "", nil)
+ _, err := libcni.InjectConf(conf, map[string]interface{}{"": nil})
Expect(err).To(MatchError(HavePrefix(`unmarshal existing network bytes`)))
})
It("returns key error", func() {
- _, err := libcni.InjectConf(testNetConfig, "", nil)
- Expect(err).To(MatchError(HavePrefix(`key value can not be empty`)))
+ _, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"": nil})
+ Expect(err).To(MatchError(HavePrefix(`keys cannot be empty`)))
})
It("returns newValue error", func() {
- _, err := libcni.InjectConf(testNetConfig, "test", nil)
- Expect(err).To(MatchError(HavePrefix(`newValue must be specified`)))
+ _, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": nil})
+ Expect(err).To(MatchError(HavePrefix(`key 'test' value must not be nil`)))
})
})
It("adds the new key & value to the config", func() {
newPluginConfig := []byte(`{"name":"some-plugin","test":"test"}`)
- resultConfig, err := libcni.InjectConf(testNetConfig, "test", "test")
+ resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
Network: &types.NetConf{Name: "some-plugin"},
It("adds the new value for exiting key", func() {
newPluginConfig := []byte(`{"name":"some-plugin","test":"changedValue"}`)
- resultConfig, err := libcni.InjectConf(testNetConfig, "test", "test")
+ resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred())
- resultConfig, err = libcni.InjectConf(resultConfig, "test", "changedValue")
+ resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "changedValue"})
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
It("adds existing key & value", func() {
newPluginConfig := []byte(`{"name":"some-plugin","test":"test"}`)
- resultConfig, err := libcni.InjectConf(testNetConfig, "test", "test")
+ resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred())
- resultConfig, err = libcni.InjectConf(resultConfig, "test", "test")
+ resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"test": "test"})
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{
newDNS := &types.DNS{Nameservers: servers, Domain: "local"}
// inject DNS
- resultConfig, err := libcni.InjectConf(testNetConfig, "dns", newDNS)
+ resultConfig, err := libcni.InjectConf(testNetConfig, map[string]interface{}{"dns": newDNS})
Expect(err).NotTo(HaveOccurred())
// inject type
- resultConfig, err = libcni.InjectConf(resultConfig, "type", "bridge")
+ resultConfig, err = libcni.InjectConf(resultConfig, map[string]interface{}{"type": "bridge"})
Expect(err).NotTo(HaveOccurred())
Expect(resultConfig).To(Equal(&libcni.NetworkConfig{