From 71c4a60741c87e17325ea6b03234a38fabd06986 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Thu, 16 Feb 2017 22:57:12 -0600 Subject: [PATCH] spec,libcni: add support for injecting runtimeConfig into plugin stdin data Add a new CapabilityArgs member to the RuntimeConf struct which runtimes can use to pass arbitrary capability-based keys to the plugin. Elements of this member will be filtered against the plugin's advertised capabilities (from its config JSON) and then added to a new "runtimeConfig" top-level map added to the config JSON sent to the plugin on stdin. Also "runtime_config"->"runtimeConfig" in CONVENTIONS.md to make capitalization consistent with other CNI config keys like "cniVersion". --- CONVENTIONS.md | 12 +- SPEC.md | 4 +- libcni/api.go | 70 ++++++++++-- libcni/api_test.go | 264 +++++++++++++++++++++++++++++++++++++------- libcni/conf.go | 18 +-- libcni/conf_test.go | 51 +++++++-- pkg/types/types.go | 7 +- 7 files changed, 345 insertions(+), 81 deletions(-) diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 894b73d2..b7fee04e 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -29,18 +29,18 @@ This method of passing information to a plugin is recommended when the following * The configuration has specific meaning to the plugin (i.e. it's not just general meta data) * the plugin is expected to act on the configuration or return an error if it can't -Dynamic information (i.e. data that a runtime fills out) should be placed in a `runtime_config` section. +Dynamic information (i.e. data that a runtime fills out) should be placed in a `runtimeConfig` section. | Area | Purpose| Spec and Example | Runtime implementations | Plugin Implementations | | ------ | ------ | ------ | ------ | ------ | ------ | -| port mappings | Pass mapping from ports on the host to ports in the container network namespace. | Operators can ask runtimes to pass port mapping information to plugins, by setting the following in the CNI config
"capabilities": {port_mappings": true} 
Runtimes should fill in the actual port mappings when the config is passed to plugins. It should be placed in a new section of the config "runtime_config" e.g.
"runtime_config": {
"port_mappings" : [
{ "host_port": 8080, "container_port": 80, "protocol": "tcp" },
{ "host_port": 8000, "container_port": 8001, "protocol": "udp" }
]
}
| none | none | +| port mappings | Pass mapping from ports on the host to ports in the container network namespace. | Operators can ask runtimes to pass port mapping information to plugins, by setting the following in the CNI config
"capabilities": {"portMappings": true} 
Runtimes should fill in the actual port mappings when the config is passed to plugins. It should be placed in a new section of the config "runtimeConfig" e.g.
"runtimeConfig": {
"portMappings" : [
{ "hostPort": 8080, "containerPort": 80, "protocol": "tcp" },
{ "hostPort": 8000, "containerPort": 8001, "protocol": "udp" }
]
}
| none | none | For example, the configuration for a port mapping plugin might look like this to an operator (it should be included as part of a [network configuration list](https://github.com/containernetworking/cni/blob/master/SPEC.md#network-configuration-lists). ```json { "name" : "ExamplePlugin", "type" : "port-mapper", - "capabilities": {"port_mappings": true} + "capabilities": {"portMappings": true} } ``` @@ -49,9 +49,9 @@ But the runtime would fill in the mappings so the plugin itself would receive so { "name" : "ExamplePlugin", "type" : "port-mapper", - "runtime_config": { - "port_mappings": [ - {"host_port": 8080, "container_port": 80, "protocol": "tcp"} + "runtimeConfig": { + "portMappings": [ + {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"} ] } } diff --git a/SPEC.md b/SPEC.md index 67c2c079..52295d68 100644 --- a/SPEC.md +++ b/SPEC.md @@ -253,8 +253,8 @@ The list is described in JSON form, and can be stored on disk or generated from - `name` (string): Network name. This should be unique across all containers on the host (or other administrative domain). - `plugins` (list): A list of standard CNI network configuration dictionaries (see above). -When executing a plugin list, the runtime MUST replace the `name` and `cniVersion` fields in each individual network configuration in the list with the `name` and `cniVersion` field of the list itself. -This ensures that the name and CNI version is the same for all plugin executions in the list, preventing versioning conflicts between plugins. +When executing a plugin list, the runtime MUST replace the `name` and `cniVersion` fields in each individual network configuration in the list with the `name` and `cniVersion` field of the list itself. This ensures that the name and CNI version is the same for all plugin executions in the list, preventing versioning conflicts between plugins. +The runtime may also pass capability-based keys as a map in the top-level `runtimeConfig` key of the plugin's config JSON if a plugin advertises it supports a specific capability via the `capabilities` key of its network configuration. The key passed in `runtimeConfig` MUST match the name of the specific capability from the `capabilities` key of the plugins network configuration. See CONVENTIONS.md for more information on capabilities and how they are sent to plugins via the `runtimeConfig` key. For the ADD action, the runtime MUST also add a `prevResult` field to the configuration JSON of any plugin after the first one, which MUST be the Result of the previous plugin (if any) in JSON format ([see below](#network-configuration-list-runtime-examples)). For the ADD action, plugins SHOULD echo the contents of the `prevResult` field to their stdout to allow subsequent plugins (and the runtime) to receive the result, unless they wish to modify or suppress a previous result. diff --git a/libcni/api.go b/libcni/api.go index 50531fa6..a23cbb2c 100644 --- a/libcni/api.go +++ b/libcni/api.go @@ -28,6 +28,12 @@ type RuntimeConf struct { 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 { @@ -57,22 +63,54 @@ type CNIConfig 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 - // Ensure every config uses the same name and version - orig, err = InjectConf(orig, "name", list.Name) - if err != nil { - return nil, err + inject := map[string]interface{}{ + "name": list.Name, + "cniVersion": list.CNIVersion, } - orig, err = InjectConf(orig, "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, inject) if err != nil { return nil, err } - // Add previous plugin result - if prevResult != nil { - orig, err = InjectConf(orig, "prevResult", prevResult) + 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 + } + } + + if len(rc) > 0 { + orig, err = InjectConf(orig, map[string]interface{}{"runtimeConfig": rc}) if err != nil { return nil, err } @@ -90,7 +128,7 @@ func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (ty return nil, err } - newConf, err := buildOneConfig(list, net, prevResult) + newConf, err := buildOneConfig(list, net, prevResult, rt) if err != nil { return nil, err } @@ -114,7 +152,7 @@ func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) err return err } - newConf, err := buildOneConfig(list, net, nil) + newConf, err := buildOneConfig(list, net, nil, rt) if err != nil { return err } @@ -134,6 +172,11 @@ func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Resul return nil, err } + net, err = injectRuntimeConfig(net, rt) + if err != nil { + return nil, err + } + return invoke.ExecPluginWithResult(pluginPath, net.Bytes, c.args("ADD", rt)) } @@ -144,6 +187,11 @@ func (c *CNIConfig) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error { return err } + net, err = injectRuntimeConfig(net, rt) + if err != nil { + return err + } + return invoke.ExecPluginWithoutResult(pluginPath, net.Bytes, c.args("DEL", rt)) } diff --git a/libcni/api_test.go b/libcni/api_test.go index e228c965..5f5207c2 100644 --- a/libcni/api_test.go +++ b/libcni/api_test.go @@ -19,6 +19,7 @@ import ( "fmt" "io/ioutil" "net" + "os" "path/filepath" "github.com/containernetworking/cni/libcni" @@ -35,19 +36,25 @@ type pluginInfo struct { 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()) @@ -58,23 +65,155 @@ func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePat } 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 @@ -99,12 +238,19 @@ var _ = Describe("Invoking plugins", func() { } 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), } @@ -112,19 +258,36 @@ var _ = Describe("Invoking plugins", func() { 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) @@ -149,6 +312,7 @@ var _ = Describe("Invoking plugins", func() { 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() { @@ -184,6 +348,7 @@ var _ = Describe("Invoking plugins", 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() { @@ -241,10 +406,49 @@ var _ = Describe("Invoking plugins", 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", @@ -256,25 +460,13 @@ var _ = Describe("Invoking plugins", func() { ] }`, 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()) } }) @@ -307,13 +499,10 @@ var _ = Describe("Invoking plugins", func() { 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)) } }) @@ -351,13 +540,10 @@ var _ = Describe("Invoking plugins", func() { 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)) } }) diff --git a/libcni/conf.go b/libcni/conf.go index 8257d9f1..4fa2bfed 100644 --- a/libcni/conf.go +++ b/libcni/conf.go @@ -201,22 +201,24 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { return ConfListFromConf(singleConf) } -func InjectConf(original *NetworkConfig, key string, newValue interface{}) (*NetworkConfig, error) { +func InjectConf(original *NetworkConfig, newValues map[string]interface{}) (*NetworkConfig, error) { config := make(map[string]interface{}) err := json.Unmarshal(original.Bytes, &config) if err != nil { return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) } - if key == "" { - return nil, fmt.Errorf("key value can not be empty") - } + for key, value := range newValues { + if key == "" { + return nil, fmt.Errorf("keys cannot be empty") + } - if newValue == nil { - return nil, fmt.Errorf("newValue must be specified") - } + if value == nil { + return nil, fmt.Errorf("key '%s' value must not be nil", key) + } - config[key] = newValue + config[key] = value + } newBytes, err := json.Marshal(config) if err != nil { diff --git a/libcni/conf_test.go b/libcni/conf_test.go index de68d98c..b9bd64ef 100644 --- a/libcni/conf_test.go +++ b/libcni/conf_test.go @@ -115,6 +115,33 @@ var _ = Describe("Loading configuration from disk", func() { }) }) + 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() { @@ -286,18 +313,18 @@ var _ = Describe("Loading configuration from disk", 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`))) }) }) @@ -305,7 +332,7 @@ var _ = Describe("Loading configuration from disk", func() { 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"}, @@ -316,10 +343,10 @@ var _ = Describe("Loading configuration from disk", func() { 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{ @@ -331,10 +358,10 @@ var _ = Describe("Loading configuration from disk", func() { 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{ @@ -350,11 +377,11 @@ var _ = Describe("Loading configuration from disk", func() { 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{ diff --git a/pkg/types/types.go b/pkg/types/types.go index b7c27de1..3263015a 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -60,9 +60,10 @@ func (n *IPNet) UnmarshalJSON(data []byte) error { type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - IPAM struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + Capabilities map[string]bool `json:"capabilities,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"`