diff --git a/libcni/api.go b/libcni/api.go index dfc30cab..9a82dc34 100644 --- a/libcni/api.go +++ b/libcni/api.go @@ -34,7 +34,17 @@ type NetworkConfig struct { Bytes []byte } +type NetworkConfigList struct { + Name string + CNIVersion string + Plugins []*NetworkConfig + Bytes []byte +} + type CNI interface { + AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (*types.Result, error) + DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error + AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) DelNetwork(net *NetworkConfig, rt *RuntimeConf) error } @@ -46,6 +56,76 @@ type CNIConfig struct { // CNIConfig implements the CNI interface var _ CNI = &CNIConfig{} +func buildOneConfig(list *NetworkConfigList, orig *NetworkConfig, prevResult *types.Result) (*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 + } + orig, err = InjectConf(orig, "cniVersion", list.CNIVersion) + if err != nil { + return nil, err + } + + // Add previous plugin result + if prevResult != nil { + orig, err = InjectConf(orig, "prevResult", prevResult) + if err != nil { + return nil, err + } + } + + return orig, nil +} + +// AddNetworkList executes a sequence of plugins with the ADD command +func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (*types.Result, error) { + var prevResult *types.Result + for _, net := range list.Plugins { + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return nil, err + } + + newConf, err := buildOneConfig(list, net, prevResult) + if err != nil { + return nil, err + } + + prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt)) + if err != nil { + return nil, err + } + } + + return prevResult, nil +} + +// DelNetworkList executes a sequence of plugins with the DEL command +func (c *CNIConfig) DelNetworkList(list *NetworkConfigList, rt *RuntimeConf) error { + for i := len(list.Plugins) - 1; i >= 0; i-- { + net := list.Plugins[i] + + pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) + if err != nil { + return err + } + + newConf, err := buildOneConfig(list, net, nil) + if err != nil { + return err + } + + if err := invoke.ExecPluginWithoutResult(pluginPath, newConf.Bytes, c.args("DEL", rt)); err != nil { + return err + } + } + + return nil +} + // AddNetwork executes the plugin with the ADD command func (c *CNIConfig) AddNetwork(net *NetworkConfig, rt *RuntimeConf) (*types.Result, error) { pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path) diff --git a/libcni/api_test.go b/libcni/api_test.go index c71d1971..bc9f06d6 100644 --- a/libcni/api_test.go +++ b/libcni/api_test.go @@ -15,6 +15,8 @@ package libcni_test import ( + "encoding/json" + "fmt" "io/ioutil" "net" "path/filepath" @@ -28,150 +30,337 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("Invoking the plugin", func() { - var ( - debugFilePath string - debug *noop_debug.Debug - cniBinPath string - pluginConfig string - cniConfig libcni.CNIConfig - netConfig *libcni.NetworkConfig - runtimeConfig *libcni.RuntimeConf +type pluginInfo struct { + debugFilePath string + debug *noop_debug.Debug + config string +} - expectedCmdArgs skel.CmdArgs - ) +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) + } + obj["name"] = name + return json.Marshal(obj) +} - BeforeEach(func() { - debugFile, err := ioutil.TempFile("", "cni_debug") - Expect(err).NotTo(HaveOccurred()) - Expect(debugFile.Close()).To(Succeed()) - debugFilePath = debugFile.Name() +func newPluginInfo(configKey, configValue, prevResult string, injectDebugFilePath bool, result string) pluginInfo { + debugFile, err := ioutil.TempFile("", "cni_debug") + Expect(err).NotTo(HaveOccurred()) + Expect(debugFile.Close()).To(Succeed()) + debugFilePath := debugFile.Name() - debug = &noop_debug.Debug{ - ReportResult: `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }`, - } - Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) + debug := &noop_debug.Debug{ + ReportResult: result, + } + Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) - cniBinPath = filepath.Dir(pluginPaths["noop"]) - pluginConfig = `{ "type": "noop", "some-key": "some-value", "cniVersion": "0.2.0" }` - cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}} - netConfig = &libcni.NetworkConfig{ - Network: &types.NetConf{ - Type: "noop", - }, - Bytes: []byte(pluginConfig), - } - runtimeConfig = &libcni.RuntimeConf{ - ContainerID: "some-container-id", - NetNS: "/some/netns/path", - IfName: "some-eth0", - Args: [][2]string{[2]string{"DEBUG", debugFilePath}}, - } + config := fmt.Sprintf(`{"type": "noop", "%s": "%s", "cniVersion": "0.2.0"`, configKey, configValue) + if prevResult != "" { + config += fmt.Sprintf(`, "prevResult": %s`, prevResult) + } + if injectDebugFilePath { + config += fmt.Sprintf(`, "debugFile": "%s"`, debugFilePath) + } + config += "}" - expectedCmdArgs = skel.CmdArgs{ - ContainerID: "some-container-id", - Netns: "/some/netns/path", - IfName: "some-eth0", - Args: "DEBUG=" + debugFilePath, - Path: cniBinPath, - StdinData: []byte(pluginConfig), - } - }) + return pluginInfo{ + debugFilePath: debugFilePath, + debug: debug, + config: config, + } +} - Describe("AddNetwork", func() { - It("executes the plugin with command ADD", func() { - result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) - Expect(err).NotTo(HaveOccurred()) +var _ = Describe("Invoking plugins", func() { + Describe("Invoking a single plugin", func() { + var ( + plugin pluginInfo + cniBinPath string + cniConfig libcni.CNIConfig + netConfig *libcni.NetworkConfig + runtimeConfig *libcni.RuntimeConf - Expect(result).To(Equal(&types.Result{ - IP4: &types.IPConfig{ - IP: net.IPNet{ - IP: net.ParseIP("10.1.2.3"), - Mask: net.IPv4Mask(255, 255, 255, 0), - }, + expectedCmdArgs skel.CmdArgs + ) + + BeforeEach(func() { + pluginResult := `{ "ip4": { "ip": "10.1.2.3/24" }, "dns": {} }` + plugin = newPluginInfo("some-key", "some-value", "", false, pluginResult) + + cniBinPath = filepath.Dir(pluginPaths["noop"]) + cniConfig = libcni.CNIConfig{Path: []string{cniBinPath}} + netConfig = &libcni.NetworkConfig{ + Network: &types.NetConf{ + Type: "noop", }, - })) + Bytes: []byte(plugin.config), + } + runtimeConfig = &libcni.RuntimeConf{ + ContainerID: "some-container-id", + NetNS: "/some/netns/path", + IfName: "some-eth0", + Args: [][2]string{{"DEBUG", plugin.debugFilePath}}, + } - debug, err := noop_debug.ReadDebug(debugFilePath) - Expect(err).NotTo(HaveOccurred()) - Expect(debug.Command).To(Equal("ADD")) - Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + expectedCmdArgs = skel.CmdArgs{ + ContainerID: "some-container-id", + Netns: "/some/netns/path", + IfName: "some-eth0", + Args: "DEBUG=" + plugin.debugFilePath, + Path: cniBinPath, + StdinData: []byte(plugin.config), + } }) - Context("when finding the plugin fails", func() { - BeforeEach(func() { - netConfig.Network.Type = "does-not-exist" - }) - - It("returns the error", func() { - _, err := cniConfig.AddNetwork(netConfig, runtimeConfig) - Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) - }) - }) - - Context("when the plugin errors", func() { - BeforeEach(func() { - debug.ReportError = "plugin error: banana" - Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) - }) - It("unmarshals and returns the error", func() { + Describe("AddNetwork", func() { + It("executes the plugin with command ADD", func() { result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) - Expect(result).To(BeNil()) - Expect(err).To(MatchError("plugin error: banana")) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal(&types.Result{ + IP4: &types.IPConfig{ + IP: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + })) + + debug, err := noop_debug.ReadDebug(plugin.debugFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(debug.Command).To(Equal("ADD")) + Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + }) + + Context("when finding the plugin fails", func() { + BeforeEach(func() { + netConfig.Network.Type = "does-not-exist" + }) + + It("returns the error", func() { + _, err := cniConfig.AddNetwork(netConfig, runtimeConfig) + Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + }) + }) + + Context("when the plugin errors", func() { + BeforeEach(func() { + plugin.debug.ReportError = "plugin error: banana" + Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) + }) + It("unmarshals and returns the error", func() { + result, err := cniConfig.AddNetwork(netConfig, runtimeConfig) + Expect(result).To(BeNil()) + Expect(err).To(MatchError("plugin error: banana")) + }) + }) + }) + + Describe("DelNetwork", func() { + It("executes the plugin with command DEL", func() { + err := cniConfig.DelNetwork(netConfig, runtimeConfig) + Expect(err).NotTo(HaveOccurred()) + + debug, err := noop_debug.ReadDebug(plugin.debugFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(debug.Command).To(Equal("DEL")) + Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + }) + + Context("when finding the plugin fails", func() { + BeforeEach(func() { + netConfig.Network.Type = "does-not-exist" + }) + + It("returns the error", func() { + err := cniConfig.DelNetwork(netConfig, runtimeConfig) + Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + }) + }) + + Context("when the plugin errors", func() { + BeforeEach(func() { + plugin.debug.ReportError = "plugin error: banana" + Expect(plugin.debug.WriteDebug(plugin.debugFilePath)).To(Succeed()) + }) + It("unmarshals and returns the error", func() { + err := cniConfig.DelNetwork(netConfig, runtimeConfig) + Expect(err).To(MatchError("plugin error: banana")) + }) + }) + }) + + Describe("GetVersionInfo", func() { + It("executes the plugin with the command VERSION", func() { + versionInfo, err := cniConfig.GetVersionInfo("noop") + Expect(err).NotTo(HaveOccurred()) + + Expect(versionInfo).NotTo(BeNil()) + Expect(versionInfo.SupportedVersions()).To(Equal([]string{ + "0.-42.0", "0.1.0", "0.2.0", + })) + }) + + Context("when finding the plugin fails", func() { + It("returns the error", func() { + _, err := cniConfig.GetVersionInfo("does-not-exist") + Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + }) }) }) }) - Describe("DelNetwork", func() { - It("executes the plugin with command DEL", func() { - err := cniConfig.DelNetwork(netConfig, runtimeConfig) + Describe("Invoking a plugin list", func() { + var ( + plugins []pluginInfo + cniBinPath string + cniConfig libcni.CNIConfig + netConfigList *libcni.NetworkConfigList + runtimeConfig *libcni.RuntimeConf + + expectedCmdArgs skel.CmdArgs + ) + + BeforeEach(func() { + plugins = make([]pluginInfo, 3, 3) + plugins[0] = newPluginInfo("some-key", "some-value", "", true, `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`) + plugins[1] = newPluginInfo("some-key", "some-other-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "PASSTHROUGH") + plugins[2] = newPluginInfo("some-key", "yet-another-value", `{"dns":{},"ip4":{"ip": "10.1.2.3/24"}}`, true, "INJECT-DNS") + + configList := []byte(fmt.Sprintf(`{ + "name": "some-list", + "cniVersion": "0.2.0", + "plugins": [ + %s, + %s, + %s + ] +}`, plugins[0].config, plugins[1].config, plugins[2].config)) + + var err error + netConfigList, err = libcni.ConfListFromBytes(configList) Expect(err).NotTo(HaveOccurred()) - debug, err := noop_debug.ReadDebug(debugFilePath) - Expect(err).NotTo(HaveOccurred()) - Expect(debug.Command).To(Equal("DEL")) - Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + 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, + } }) - Context("when finding the plugin fails", func() { - BeforeEach(func() { - netConfig.Network.Type = "does-not-exist" + Describe("AddNetworkList", func() { + It("executes all plugins with command ADD and returns an intermediate result", func() { + result, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig) + Expect(err).NotTo(HaveOccurred()) + + Expect(result).To(Equal(&types.Result{ + // IP4 added by first plugin + IP4: &types.IPConfig{ + IP: net.IPNet{ + IP: net.ParseIP("10.1.2.3"), + Mask: net.IPv4Mask(255, 255, 255, 0), + }, + }, + // DNS injected by last plugin + DNS: types.DNS{ + Nameservers: []string{"1.2.3.4"}, + }, + })) + + for i := 0; i < len(plugins); i++ { + 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 + debug.CmdArgs.StdinData = nil + Expect(debugJSON).To(MatchJSON(newConfig)) + Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + } }) - It("returns the error", func() { - err := cniConfig.DelNetwork(netConfig, runtimeConfig) - Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + Context("when finding the plugin fails", func() { + BeforeEach(func() { + netConfigList.Plugins[1].Network.Type = "does-not-exist" + }) + + It("returns the error", func() { + _, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig) + Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + }) + }) + + Context("when the second plugin errors", func() { + BeforeEach(func() { + plugins[1].debug.ReportError = "plugin error: banana" + Expect(plugins[1].debug.WriteDebug(plugins[1].debugFilePath)).To(Succeed()) + }) + It("unmarshals and returns the error", func() { + result, err := cniConfig.AddNetworkList(netConfigList, runtimeConfig) + Expect(result).To(BeNil()) + Expect(err).To(MatchError("plugin error: banana")) + }) }) }) - Context("when the plugin errors", func() { - BeforeEach(func() { - debug.ReportError = "plugin error: banana" - Expect(debug.WriteDebug(debugFilePath)).To(Succeed()) + Describe("DelNetworkList", func() { + It("executes all the plugins in reverse order with command DEL", func() { + err := cniConfig.DelNetworkList(netConfigList, runtimeConfig) + Expect(err).NotTo(HaveOccurred()) + + for i := 0; i < len(plugins); i++ { + 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 + debug.CmdArgs.StdinData = nil + Expect(debugJSON).To(MatchJSON(newConfig)) + Expect(debug.CmdArgs).To(Equal(expectedCmdArgs)) + } }) - It("unmarshals and returns the error", func() { - err := cniConfig.DelNetwork(netConfig, runtimeConfig) - Expect(err).To(MatchError("plugin error: banana")) + + Context("when finding the plugin fails", func() { + BeforeEach(func() { + netConfigList.Plugins[1].Network.Type = "does-not-exist" + }) + + It("returns the error", func() { + err := cniConfig.DelNetworkList(netConfigList, runtimeConfig) + Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) + }) + }) + + Context("when the plugin errors", func() { + BeforeEach(func() { + plugins[1].debug.ReportError = "plugin error: banana" + Expect(plugins[1].debug.WriteDebug(plugins[1].debugFilePath)).To(Succeed()) + }) + It("unmarshals and returns the error", func() { + err := cniConfig.DelNetworkList(netConfigList, runtimeConfig) + Expect(err).To(MatchError("plugin error: banana")) + }) }) }) - }) - Describe("GetVersionInfo", func() { - It("executes the plugin with the command VERSION", func() { - versionInfo, err := cniConfig.GetVersionInfo("noop") - Expect(err).NotTo(HaveOccurred()) - - Expect(versionInfo).NotTo(BeNil()) - Expect(versionInfo.SupportedVersions()).To(Equal([]string{ - "0.-42.0", "0.1.0", "0.2.0", - })) - }) - - Context("when finding the plugin fails", func() { - It("returns the error", func() { - _, err := cniConfig.GetVersionInfo("does-not-exist") - Expect(err).To(MatchError(ContainSubstring(`failed to find plugin "does-not-exist"`))) - }) - }) }) }) diff --git a/libcni/conf.go b/libcni/conf.go index 708686e6..4a9d11be 100644 --- a/libcni/conf.go +++ b/libcni/conf.go @@ -39,7 +39,73 @@ func ConfFromFile(filename string) (*NetworkConfig, error) { return ConfFromBytes(bytes) } -func ConfFiles(dir string) ([]string, error) { +func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { + rawList := make(map[string]interface{}) + if err := json.Unmarshal(bytes, &rawList); err != nil { + return nil, fmt.Errorf("error parsing configuration list: %s", err) + } + + rawName, ok := rawList["name"] + if !ok { + return nil, fmt.Errorf("error parsing configuration list: no name") + } + name, ok := rawName.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid name type %T", rawName) + } + + var cniVersion string + rawVersion, ok := rawList["cniVersion"] + if ok { + cniVersion, ok = rawVersion.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion type %T", rawVersion) + } + } + + list := &NetworkConfigList{ + Name: name, + CNIVersion: cniVersion, + Bytes: bytes, + } + + var plugins []interface{} + plug, ok := rawList["plugins"] + if !ok { + return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key") + } + plugins, ok = plug.([]interface{}) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug) + } + if len(plugins) == 0 { + return nil, fmt.Errorf("error parsing configuration list: no plugins in list") + } + + for i, conf := range plugins { + newBytes, err := json.Marshal(conf) + if err != nil { + return nil, fmt.Errorf("Failed to marshal plugin config %d: %v", i, err) + } + netConf, err := ConfFromBytes(newBytes) + if err != nil { + return nil, fmt.Errorf("Failed to parse plugin config %d: %v", i, err) + } + list.Plugins = append(list.Plugins, netConf) + } + + return list, nil +} + +func ConfListFromFile(filename string) (*NetworkConfigList, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading %s: %s", filename, err) + } + return ConfListFromBytes(bytes) +} + +func ConfFiles(dir string, extensions []string) ([]string, error) { // In part, adapted from rkt/networking/podenv.go#listFiles files, err := ioutil.ReadDir(dir) switch { @@ -56,15 +122,17 @@ func ConfFiles(dir string) ([]string, error) { continue } fileExt := filepath.Ext(f.Name()) - if fileExt == ".conf" || fileExt == ".json" { - confFiles = append(confFiles, filepath.Join(dir, f.Name())) + for _, ext := range extensions { + if fileExt == ext { + confFiles = append(confFiles, filepath.Join(dir, f.Name())) + } } } return confFiles, nil } func LoadConf(dir, name string) (*NetworkConfig, error) { - files, err := ConfFiles(dir) + files, err := ConfFiles(dir, []string{".conf", ".json"}) switch { case err != nil: return nil, err @@ -85,6 +153,28 @@ func LoadConf(dir, name string) (*NetworkConfig, error) { return nil, fmt.Errorf(`no net configuration with name "%s" in %s`, name, dir) } +func LoadConfList(dir, name string) (*NetworkConfigList, error) { + files, err := ConfFiles(dir, []string{".conflist"}) + switch { + case err != nil: + return nil, err + case len(files) == 0: + return nil, fmt.Errorf("no net configuration lists found") + } + sort.Strings(files) + + for _, confFile := range files { + conf, err := ConfListFromFile(confFile) + if err != nil { + return nil, err + } + if conf.Name == name { + return conf, nil + } + } + return nil, fmt.Errorf(`no net configuration list with name "%s" in %s`, name, dir) +} + func InjectConf(original *NetworkConfig, key string, newValue interface{}) (*NetworkConfig, error) { config := make(map[string]interface{}) err := json.Unmarshal(original.Bytes, &config) diff --git a/libcni/conf_test.go b/libcni/conf_test.go index 4d032ba0..466e440b 100644 --- a/libcni/conf_test.go +++ b/libcni/conf_test.go @@ -26,29 +26,25 @@ import ( ) var _ = Describe("Loading configuration from disk", func() { - var ( - configDir string - pluginConfig []byte - testNetConfig *libcni.NetworkConfig - ) - - BeforeEach(func() { - var err error - configDir, err = ioutil.TempDir("", "plugin-conf") - Expect(err).NotTo(HaveOccurred()) - - pluginConfig = []byte(`{ "name": "some-plugin", "some-key": "some-value" }`) - Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed()) - - testNetConfig = &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"}, - Bytes: []byte(`{ "name": "some-plugin" }`)} - }) - - AfterEach(func() { - Expect(os.RemoveAll(configDir)).To(Succeed()) - }) - Describe("LoadConf", func() { + var ( + configDir string + pluginConfig []byte + ) + + BeforeEach(func() { + var err error + configDir, err = ioutil.TempDir("", "plugin-conf") + Expect(err).NotTo(HaveOccurred()) + + pluginConfig = []byte(`{ "name": "some-plugin", "some-key": "some-value" }`) + Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conf"), pluginConfig, 0600)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.RemoveAll(configDir)).To(Succeed()) + }) + It("finds the network config file for the plugin of the given type", func() { netConfig, err := libcni.LoadConf(configDir, "some-plugin") Expect(err).NotTo(HaveOccurred()) @@ -128,7 +124,137 @@ var _ = Describe("Loading configuration from disk", func() { }) }) + Describe("LoadConfList", func() { + var ( + configDir string + configList []byte + ) + + BeforeEach(func() { + var err error + configDir, err = ioutil.TempDir("", "plugin-conf") + Expect(err).NotTo(HaveOccurred()) + + configList = []byte(`{ + "name": "some-list", + "cniVersion": "0.2.0", + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + }, + { + "type": "bridge", + "mtu": 1400 + }, + { + "type": "port-forwarding", + "ports": {"20.0.0.1:8080": "80"} + } + ] +}`) + Expect(ioutil.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0600)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.RemoveAll(configDir)).To(Succeed()) + }) + + It("finds the network config file for the plugin of the given type", func() { + netConfigList, err := libcni.LoadConfList(configDir, "some-list") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList).To(Equal(&libcni.NetworkConfigList{ + Name: "some-list", + CNIVersion: "0.2.0", + Plugins: []*libcni.NetworkConfig{ + { + Network: &types.NetConf{Type: "host-local"}, + Bytes: []byte(`{"subnet":"10.0.0.1/24","type":"host-local"}`), + }, + { + Network: &types.NetConf{Type: "bridge"}, + Bytes: []byte(`{"mtu":1400,"type":"bridge"}`), + }, + { + Network: &types.NetConf{Type: "port-forwarding"}, + Bytes: []byte(`{"ports":{"20.0.0.1:8080":"80"},"type":"port-forwarding"}`), + }, + }, + Bytes: configList, + })) + }) + + Context("when the config directory does not exist", func() { + BeforeEach(func() { + Expect(os.RemoveAll(configDir)).To(Succeed()) + }) + + It("returns a useful error", func() { + _, err := libcni.LoadConfList(configDir, "some-plugin") + Expect(err).To(MatchError("no net configuration lists found")) + }) + }) + + Context("when there is no config for the desired plugin list", func() { + It("returns a useful error", func() { + _, err := libcni.LoadConfList(configDir, "some-other-plugin") + Expect(err).To(MatchError(ContainSubstring(`no net configuration list with name "some-other-plugin" in`))) + }) + }) + + Context("when a config file is malformed", func() { + BeforeEach(func() { + Expect(ioutil.WriteFile(filepath.Join(configDir, "00-bad.conflist"), []byte(`{`), 0600)).To(Succeed()) + }) + + It("returns a useful error", func() { + _, err := libcni.LoadConfList(configDir, "some-plugin") + Expect(err).To(MatchError(`error parsing configuration list: unexpected end of JSON input`)) + }) + }) + + Context("when the config is in a nested subdir", func() { + BeforeEach(func() { + subdir := filepath.Join(configDir, "subdir1", "subdir2") + Expect(os.MkdirAll(subdir, 0700)).To(Succeed()) + + configList = []byte(`{ + "name": "deep", + "cniVersion": "0.2.0", + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + }, + ] +}`) + Expect(ioutil.WriteFile(filepath.Join(subdir, "90-deep.conflist"), configList, 0600)).To(Succeed()) + }) + + It("will not find the config", func() { + _, err := libcni.LoadConfList(configDir, "deep") + Expect(err).To(MatchError(HavePrefix("no net configuration list with name"))) + }) + }) + }) + + Describe("ConfListFromFile", func() { + Context("when the file cannot be opened", func() { + It("returns a useful error", func() { + _, err := libcni.ConfListFromFile("/tmp/nope/not-here") + Expect(err).To(MatchError(HavePrefix(`error reading /tmp/nope/not-here: open /tmp/nope/not-here`))) + }) + }) + }) + Describe("InjectConf", func() { + var testNetConfig *libcni.NetworkConfig + + BeforeEach(func() { + testNetConfig = &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"}, + Bytes: []byte(`{ "name": "some-plugin" }`)} + }) + Context("when function parameters are incorrect", func() { It("returns unmarshal error", func() { conf := &libcni.NetworkConfig{Network: &types.NetConf{Name: "some-plugin"}, diff --git a/pkg/types/types.go b/pkg/types/types.go index 17caa49b..c1fddcd5 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -59,14 +59,23 @@ 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"` + PrevResult *Result `json:"prevResult,omitempty"` + IPAM struct { Type string `json:"type,omitempty"` } `json:"ipam,omitempty"` DNS DNS `json:"dns"` } +// NetConfList describes an ordered list of networks. +type NetConfList struct { + CNIVersion string `json:"cniVersion,omitempty"` + + Name string `json:"name,omitempty"` + Plugins []*NetConf `json:"plugins,omitempty"` +} + // Result is what gets returned from the plugin (via stdout) to the caller type Result struct { IP4 *IPConfig `json:"ip4,omitempty"` diff --git a/plugins/test/noop/main.go b/plugins/test/noop/main.go index f268890c..89370ef5 100644 --- a/plugins/test/noop/main.go +++ b/plugins/test/noop/main.go @@ -37,13 +37,14 @@ import ( type NetConf struct { types.NetConf - DebugFile string `json:"debugFile"` + DebugFile string `json:"debugFile"` + PrevResult *types.Result `json:"prevResult,omitempty"` } func loadConf(bytes []byte) (*NetConf, error) { n := &NetConf{} if err := json.Unmarshal(bytes, n); err != nil { - return nil, fmt.Errorf("failed to load netconf: %v", err) + return nil, fmt.Errorf("failed to load netconf: %v %q", err, string(bytes)) } return n, nil } @@ -66,15 +67,15 @@ func parseExtraArgs(args string) (map[string]string, error) { return m, nil } -func getDebugFilePath(stdinData []byte, args string) (string, error) { +func getConfig(stdinData []byte, args string) (string, *NetConf, error) { netConf, err := loadConf(stdinData) if err != nil { - return "", err + return "", nil, err } extraArgs, err := parseExtraArgs(args) if err != nil { - return "", err + return "", nil, err } debugFilePath, ok := extraArgs["DEBUG"] @@ -82,11 +83,11 @@ func getDebugFilePath(stdinData []byte, args string) (string, error) { debugFilePath = netConf.DebugFile } - return debugFilePath, nil + return debugFilePath, netConf, nil } func debugBehavior(args *skel.CmdArgs, command string) error { - debugFilePath, err := getDebugFilePath(args.StdinData, args.Args) + debugFilePath, netConf, err := getConfig(args.StdinData, args.Args) if err != nil { return err } @@ -118,6 +119,15 @@ func debugBehavior(args *skel.CmdArgs, command string) error { if debug.ReportError != "" { return errors.New(debug.ReportError) + } else if debug.ReportResult == "PASSTHROUGH" || debug.ReportResult == "INJECT-DNS" { + if debug.ReportResult == "INJECT-DNS" { + netConf.PrevResult.DNS.Nameservers = []string{"1.2.3.4"} + } + newResult, err := json.Marshal(netConf.PrevResult) + if err != nil { + return fmt.Errorf("failed to marshal new result: %v", err) + } + os.Stdout.WriteString(string(newResult)) } else { os.Stdout.WriteString(debug.ReportResult) } @@ -132,7 +142,7 @@ func debugGetSupportedVersions(stdinData []byte) []string { return vers } - debugFilePath, err := getDebugFilePath(stdinData, cniArgs) + debugFilePath, _, err := getConfig(stdinData, cniArgs) if err != nil { panic("test setup error: unable to get debug file path: " + err.Error()) } diff --git a/plugins/test/noop/noop_test.go b/plugins/test/noop/noop_test.go index 18f1d0c8..3573985a 100644 --- a/plugins/test/noop/noop_test.go +++ b/plugins/test/noop/noop_test.go @@ -96,6 +96,45 @@ var _ = Describe("No-op plugin", func() { Eventually(session).Should(gexec.Exit(2)) }) + It("pass previous result through when ReportResult is PASSTHROUGH", func() { + debug = &noop_debug.Debug{ReportResult: "PASSTHROUGH"} + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + + cmd.Stdin = strings.NewReader(`{ + "some":"stdin-json", + "cniVersion": "0.2.0", + "prevResult": { + "ip4": {"ip": "10.1.2.15/24"} + } +}`) + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(session.Out.Contents()).To(MatchJSON(`{"ip4": {"ip": "10.1.2.15/24"}, "dns": {}}`)) + }) + + It("injects DNS into previous result when ReportResult is INJECT-DNS", func() { + debug = &noop_debug.Debug{ReportResult: "INJECT-DNS"} + Expect(debug.WriteDebug(debugFileName)).To(Succeed()) + + cmd.Stdin = strings.NewReader(`{ + "some":"stdin-json", + "cniVersion": "0.2.0", + "prevResult": { + "ip4": {"ip": "10.1.2.3/24"}, + "dns": {} + } +}`) + + session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter) + Expect(err).NotTo(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(session.Out.Contents()).To(MatchJSON(`{ + "ip4": {"ip": "10.1.2.3/24"}, + "dns": {"nameservers": ["1.2.3.4"]} +}`)) + }) + It("allows passing debug file in config JSON", func() { // Remove the DEBUG option from CNI_ARGS and regular args newArgs := "FOO=BAR"