From a33b3b3f4cea444f256ab62c1fb137bb1a1028d0 Mon Sep 17 00:00:00 2001 From: Benjamin Leggett Date: Thu, 1 Feb 2024 15:58:07 -0500 Subject: [PATCH] Fixup as per comments Signed-off-by: Benjamin Leggett --- SPEC.md | 83 +++++++++++++-- cnitool/cnitool.go | 2 +- libcni/api.go | 11 +- libcni/conf.go | 115 ++++++++++++-------- libcni/conf_test.go | 248 ++++++++++++++++++++++++++++++++++++++------ 5 files changed, 370 insertions(+), 89 deletions(-) diff --git a/SPEC.md b/SPEC.md index 4fd94c1d..678bd5a7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -111,7 +111,8 @@ A network configuration consists of a JSON object with the following keys: - `cniVersions` (string list): List of all CNI versions which this configuration supports. See [version selection](#version-selection) below. - `name` (string): Network name. This should be unique across all network configurations on a host (or other administrative domain). Must start with an alphanumeric character, optionally followed by any combination of one or more alphanumeric characters, underscore, dot (.) or hyphen (-). Must not contain characters disallowed in file paths. A path segment (such as a filesystem directory) with the same name as the network name, containing one or more plugin configuration JSON objects for that network, should exist at the same level as the network configuration object itself. - `disableCheck` (boolean): Either `true` or `false`. If `disableCheck` is `true`, runtimes must not call `CHECK` for this network configuration list. This allows an administrator to prevent `CHECK`ing where a combination of plugins is known to return spurious errors. - +- `loadPluginsFromFolder` (boolean): Either `true` or `false`. If `true`, indicates [plugin configuration objects](#plugin-configuration-objects) should be loaded from a sibling folder with the same name as the network `name` field. These sibling folders should exist at the same path as the network configuration itself. Any valid plugin configuration objects within the sibling folder will be appended to the final list of plugins for that network. If `plugins` is not present in the network configuration, `loadPluginsFromFolder` must be present, and set to true. +- `plugins` (list): A list of inlined [plugin configuration objects](#plugin-configuration-objects). If this key is populated with inlined plugin objects, and `loadPluginsFromFolder` is true, the final set of plugins for a network must consist of all the plugin objects in this list and the all the plugins loaded from the sibling folder with the same name as the network. #### Plugin configuration objects: All plugin configuration objects present in a directory with the same name as a valid network configuration must be parsed, and each plugin with a parsable configuration object must be invoked. @@ -147,13 +148,14 @@ Plugins that consume any of these configuration keys should respect their intend Plugins may define additional fields that they accept and may generate an error if called with unknown fields. Runtimes must preserve unknown fields in plugin configuration objects when transforming for execution. #### Example configuration - +Network configuration with no inlined plugin confs, and two loaded plugin confs: `/etc/cni/net.d/10-dbnet.conf`: ```jsonc { "cniVersion": "1.1.0", "cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"], "name": "dbnet", + "loadPluginsFromFolder": true, } ``` @@ -177,7 +179,7 @@ Plugins may define additional fields that they accept and may generate an error "dns": { "nameservers": [ "10.1.0.1" ] } -}, +} ``` `/etc/cni/net.d/dbnet/10-tuning.conf`: @@ -190,16 +192,83 @@ Plugins may define additional fields that they accept and may generate an error "sysctl": { "net.core.somaxconn": "500" } -}, +} ``` -`/etc/cni/net.d/dbnet/15-portmap.conf`: +Network configuration with one inlined plugin conf, and one loaded plugin conf: +`/etc/cni/net.d/10-dbnet.conf`: ```jsonc { - "type": "portmap", - "capabilities": {"portMappings": true} + "cniVersion": "1.1.0", + "cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"], + "name": "dbnet", + "loadPluginsFromFolder": true, + plugins: [ + { + "type": "bridge", + // plugin specific parameters + "bridge": "cni0", + "keyA": ["some more", "plugin specific", "configuration"], + + "ipam": { + "type": "host-local", + // ipam specific + "subnet": "10.1.0.0/16", + "gateway": "10.1.0.1", + "routes": [ + {"dst": "0.0.0.0/0"} + ] + }, + "dns": { + "nameservers": [ "10.1.0.1" ] + } + } + ] +} +``` + +`/etc/cni/net.d/dbnet/10-tuning.conf`: +```jsonc +{ + "type": "tuning", + "capabilities": { + "mac": true + }, + "sysctl": { + "net.core.somaxconn": "500" + } } +``` +Network configuration with one inlined plugin conf, and no loaded plugin conf: +`/etc/cni/net.d/10-dbnet.conf`: +```jsonc +{ + "cniVersion": "1.1.0", + "cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"], + "name": "dbnet", + "plugins": [ + { + "type": "bridge", + // plugin specific parameters + "bridge": "cni0", + "keyA": ["some more", "plugin specific", "configuration"], + + "ipam": { + "type": "host-local", + // ipam specific + "subnet": "10.1.0.0/16", + "gateway": "10.1.0.1", + "routes": [ + {"dst": "0.0.0.0/0"} + ] + }, + "dns": { + "nameservers": [ "10.1.0.1" ] + } + } + ] +} ``` ### Version considerations diff --git a/cnitool/cnitool.go b/cnitool/cnitool.go index 0ba91838..d282890b 100644 --- a/cnitool/cnitool.go +++ b/cnitool/cnitool.go @@ -66,7 +66,7 @@ func main() { if netdir == "" { netdir = DefaultNetDir } - netconf, err := libcni.LoadConfList(netdir, os.Args[2]) + netconf, err := libcni.LoadNetworkConf(netdir, os.Args[2]) if err != nil { exit(err) } diff --git a/libcni/api.go b/libcni/api.go index 510e3c4e..e7be359d 100644 --- a/libcni/api.go +++ b/libcni/api.go @@ -76,11 +76,12 @@ type PluginConfig struct { } type NetworkConfigList struct { - Name string - CNIVersion string - DisableCheck bool - Plugins []*PluginConfig - Bytes []byte + Name string + CNIVersion string + DisableCheck bool + LoadPluginsFromFolder bool + Plugins []*PluginConfig + Bytes []byte } type NetworkAttachment struct { diff --git a/libcni/conf.go b/libcni/conf.go index a32016b4..016200c4 100644 --- a/libcni/conf.go +++ b/libcni/conf.go @@ -67,17 +67,20 @@ func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) { // Given a path to a directory containing a network configuration, and the name of a network, // loads all plugin definitions found at path `networkConfPath/networkName/*.conf` func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) { - pluginConfFiles, err := ConfFiles(filepath.Join(networkConfPath, networkName), []string{".conf"}) + var pConfs []*PluginConfig + + pluginConfPath := filepath.Join(networkConfPath, networkName) + + pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"}) switch { case err != nil: - return nil, fmt.Errorf("failed to read plugin config file: %w", err) + return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err) case len(pluginConfFiles) == 0: // Having 0 plugins for a given network is not necessarily a problem, - // but return as error for caller to decide. - return nil, fmt.Errorf("no plugin config found in %s: %w", networkConfPath, err) + // but return as error for caller to decide, since they tried to load + return nil, fmt.Errorf("no plugin config found in %s", pluginConfPath) } - var pConfs []*PluginConfig for _, pluginConfFile := range pluginConfFiles { pluginConfBytes, err := os.ReadFile(pluginConfFile) if err != nil { @@ -176,15 +179,52 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) { } } + loadPluginsFromFolder := false + if rawLoadCheck, ok := rawList["loadPluginsFromFolder"]; ok { + loadPluginsFromFolder, ok = rawLoadCheck.(bool) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid loadPluginsFromFolder type %T", rawLoadCheck) + } + } + list := &NetworkConfigList{ - Name: name, - DisableCheck: disableCheck, - CNIVersion: cniVersion, - Bytes: confBytes, + Name: name, + DisableCheck: disableCheck, + LoadPluginsFromFolder: loadPluginsFromFolder, + CNIVersion: cniVersion, + Bytes: confBytes, + } + + var plugins []interface{} + plug, ok := rawList["plugins"] + // We can have a `plugins` list key in the main conf, + // We can also have `loadPluginsFromFolder == true` + // They can both be present in the same config. + // But if one of them is NOT present/false, the other *must* be there. + if !ok && !loadPluginsFromFolder { + return nil, fmt.Errorf("error parsing configuration list: `loadPluginsFromFolder` not true, and no 'plugins' key") + } else if !ok && loadPluginsFromFolder { + return list, nil } - if _, ok := rawList["plugins"]; ok { - return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key allowed, cniVersion %s must load plugins from subdirectories", version.Current()) + 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: %w", i, err) + } + netConf, err := ConfFromBytes(newBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse plugin config %d: %w", i, err) + } + list.Plugins = append(list.Plugins, netConf) } return list, nil } @@ -195,31 +235,27 @@ func NetworkConfFromFile(filename string) (*NetworkConfigList, error) { return nil, fmt.Errorf("error reading %s: %w", filename, err) } - // TODO conf, err := NetworkConfFromBytes(bytes) if err != nil { return nil, err } - plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name) - if err != nil { - return nil, err + if conf.LoadPluginsFromFolder { + plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name) + if err != nil { + return nil, err + } + conf.Plugins = append(conf.Plugins, plugins...) } - conf.Plugins = plugins - return conf, nil } // Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions -func ConfFromBytes(bytes []byte) (*PluginConfig, error) { +func ConfFromBytes(bytes []byte) (*NetworkConfig, error) { return NetworkPluginConfFromBytes(bytes) } -// TODO Are we ok, at this point, cutting a new major version of libCNI and dropping the non-list CNI config? -// -// The non-list format was dropped in 1.0 and so I feel we should. -// // Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions func ConfFromFile(filename string) (*NetworkConfig, error) { bytes, err := os.ReadFile(filename) @@ -292,6 +328,15 @@ func LoadConf(dir, name string) (*NetworkConfig, error) { // Deprecated: Use NetworkConfXXX and NetworkPluginXXX functions func LoadConfList(dir, name string) (*NetworkConfigList, error) { + return LoadNetworkConf(dir, name) +} + +// LoadNetworkConf looks at all the network configs in a given dir, +// loads and parses them all, and returns the first one with an extension of `.conf` +// that matches the provided network name predicate. +func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) { + // TODO this .conflist/.conf extension thing is confusing and inexact + // for implementors. We should pick one extension for everything and stick with it. files, err := ConfFiles(dir, []string{".conflist"}) if err != nil { return nil, err @@ -299,7 +344,7 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { sort.Strings(files) for _, confFile := range files { - conf, err := ConfListFromFile(confFile) + conf, err := NetworkConfFromFile(confFile) if err != nil { return nil, err } @@ -308,7 +353,7 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { } } - // Try and load a network configuration file (instead of list) + // Deprecated: Try and load a network configuration file (instead of list) // from the same name, then upconvert. singleConf, err := LoadConf(dir, name) if err != nil { @@ -324,28 +369,6 @@ func LoadConfList(dir, name string) (*NetworkConfigList, error) { return ConfListFromConf(singleConf) } -// LoadNetworkConf looks at all the network configs in a given dir, -// loads and parses them all, and returns the first one that matches the provided -// network name predicate. -func LoadNetworkConf(dir, name string) (*NetworkConfigList, error) { - files, err := ConfFiles(dir, []string{".conf"}) - if err != nil { - return nil, err - } - sort.Strings(files) - - for _, confFile := range files { - conf, err := NetworkConfFromFile(confFile) - if err != nil { - return nil, err - } - if conf.Name == name { - return conf, nil - } - } - return nil, fmt.Errorf("no netconfig found in %q with name %s", dir, name) -} - // InjectConf takes a PluginConfig and inserts additional values into it, ensuring the result is serializable. func InjectConf(original *PluginConfig, newValues map[string]interface{}) (*PluginConfig, error) { config := make(map[string]interface{}) diff --git a/libcni/conf_test.go b/libcni/conf_test.go index 965bebda..03820b62 100644 --- a/libcni/conf_test.go +++ b/libcni/conf_test.go @@ -181,7 +181,7 @@ var _ = Describe("Loading configuration from disk", func() { }) }) - Describe("ConfFromBytes", func() { + Describe("NetworkPluginConfFromBytes", func() { Context("when the config is missing 'type'", func() { It("returns a useful error", func() { _, err := libcni.ConfFromBytes([]byte(`{ "name": "some-plugin", "some-key": "some-value" }`)) @@ -190,7 +190,7 @@ var _ = Describe("Loading configuration from disk", func() { }) }) - Describe("LoadConfList", func() { + Describe("LoadNetworkConf", func() { var ( configDir string configList []byte @@ -202,7 +202,7 @@ var _ = Describe("Loading configuration from disk", func() { Expect(err).NotTo(HaveOccurred()) configList = []byte(`{ - "name": "some-list", + "name": "some-network", "cniVersion": "0.2.0", "disableCheck": true, "plugins": [ @@ -228,10 +228,10 @@ var _ = Describe("Loading configuration from disk", func() { }) It("finds the network config file for the plugin of the given type", func() { - netConfigList, err := libcni.LoadConfList(configDir, "some-list") + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).NotTo(HaveOccurred()) Expect(netConfigList).To(Equal(&libcni.NetworkConfigList{ - Name: "some-list", + Name: "some-network", CNIVersion: "0.2.0", DisableCheck: true, Plugins: []*libcni.PluginConfig{ @@ -255,7 +255,7 @@ var _ = Describe("Loading configuration from disk", func() { Context("when there is a config file with the same name as the list", func() { BeforeEach(func() { configFile := []byte(`{ - "name": "some-list", + "name": "some-network", "cniVersion": "0.2.0", "type": "bridge" }`) @@ -263,7 +263,7 @@ var _ = Describe("Loading configuration from disk", func() { }) It("Loads the config list first", func() { - netConfigList, err := libcni.LoadConfList(configDir, "some-list") + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).NotTo(HaveOccurred()) Expect(netConfigList.Plugins).To(HaveLen(3)) }) @@ -271,7 +271,7 @@ var _ = Describe("Loading configuration from disk", func() { It("falls back to the config file", func() { Expect(os.Remove(filepath.Join(configDir, "50-whatever.conflist"))).To(Succeed()) - netConfigList, err := libcni.LoadConfList(configDir, "some-list") + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).NotTo(HaveOccurred()) Expect(netConfigList.Plugins).To(HaveLen(1)) Expect(netConfigList.Plugins[0].Network.Type).To(Equal("bridge")) @@ -284,15 +284,15 @@ var _ = Describe("Loading configuration from disk", func() { }) It("returns a useful error", func() { - _, err := libcni.LoadConfList(configDir, "some-plugin") + _, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).To(MatchError(libcni.NoConfigsFoundError{Dir: configDir})) }) }) - Context("when there is no config for the desired plugin list", func() { + Context("when there is no config for the desired network name", func() { It("returns a useful error", func() { - _, err := libcni.LoadConfList(configDir, "some-other-plugin") - Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-plugin"})) + _, err := libcni.LoadNetworkConf(configDir, "some-other-network") + Expect(err).To(MatchError(libcni.NotFoundError{Dir: configDir, Name: "some-other-network"})) }) }) @@ -302,7 +302,7 @@ var _ = Describe("Loading configuration from disk", func() { }) It("returns a useful error", func() { - _, err := libcni.LoadConfList(configDir, "some-plugin") + _, err := libcni.LoadNetworkConf(configDir, "some-plugin") Expect(err).To(MatchError(`error parsing configuration list: unexpected end of JSON input`)) }) }) @@ -326,7 +326,7 @@ var _ = Describe("Loading configuration from disk", func() { }) It("will not find the config", func() { - _, err := libcni.LoadConfList(configDir, "deep") + _, err := libcni.LoadNetworkConf(configDir, "deep") Expect(err).To(MatchError(HavePrefix("no net configuration with name"))) }) }) @@ -334,7 +334,7 @@ var _ = Describe("Loading configuration from disk", func() { Context("when disableCheck is a string not a boolean", func() { It("will read a 'true' value and convert to boolean", func() { configList = []byte(`{ - "name": "some-list", + "name": "some-network", "cniVersion": "0.4.0", "disableCheck": "true", "plugins": [ @@ -346,14 +346,14 @@ var _ = Describe("Loading configuration from disk", func() { }`) Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) - netConfigList, err := libcni.LoadConfList(configDir, "some-list") + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).NotTo(HaveOccurred()) Expect(netConfigList.DisableCheck).To(BeTrue()) }) It("will read a 'false' value and convert to boolean", func() { configList = []byte(`{ - "name": "some-list", + "name": "some-network", "cniVersion": "0.4.0", "disableCheck": "false", "plugins": [ @@ -365,7 +365,7 @@ var _ = Describe("Loading configuration from disk", func() { }`) Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) - netConfigList, err := libcni.LoadConfList(configDir, "some-list") + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).NotTo(HaveOccurred()) Expect(netConfigList.DisableCheck).To(BeFalse()) }) @@ -373,7 +373,7 @@ var _ = Describe("Loading configuration from disk", func() { It("will return an error on an unrecognized value", func() { const badValue string = "adsfasdfasf" configList = []byte(fmt.Sprintf(`{ - "name": "some-list", + "name": "some-network", "cniVersion": "0.4.0", "disableCheck": "%s", "plugins": [ @@ -385,16 +385,204 @@ var _ = Describe("Loading configuration from disk", func() { }`, badValue)) Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) - _, err := libcni.LoadConfList(configDir, "some-list") + _, err := libcni.LoadNetworkConf(configDir, "some-network") Expect(err).To(MatchError(fmt.Sprintf("error parsing configuration list: invalid disableCheck value \"%s\"", badValue))) }) }) + + Context("for loadPluginsFromFolder", func() { + It("the value will be parsed", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": true, + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + } + ] + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + dirPluginConf := []byte(`{ + "type": "bro-check-out-my-plugin", + "subnet": "10.0.0.1/24" + }`) + + subDir := filepath.Join(configDir, "some-network") + Expect(os.MkdirAll(subDir, 0o700)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed()) + + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList.LoadPluginsFromFolder).To(BeTrue()) + }) + + It("the value will be false if not in config", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + } + ] + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList.LoadPluginsFromFolder).To(BeFalse()) + }) + + It("will return an error on an unrecognized value", func() { + const badValue string = "spagnum" + configList = []byte(fmt.Sprintf(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": "%s", + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + } + ] + }`, badValue)) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + _, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).To(MatchError("error parsing configuration list: invalid loadPluginsFromFolder type string")) + }) + + It("will return an error if `plugins` is missing and `loadPluginsFromFolder` is also missing", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0" + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + _, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).To(MatchError("error parsing configuration list: `loadPluginsFromFolder` not true, and no 'plugins' key")) + }) + + It("will return no error if `plugins` is missing and `loadPluginsFromFolder` is true", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": true + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + dirPluginConf := []byte(`{ + "type": "bro-check-out-my-plugin", + "subnet": "10.0.0.1/24" + }`) + + subDir := filepath.Join(configDir, "some-network") + Expect(os.MkdirAll(subDir, 0o700)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed()) + + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList.LoadPluginsFromFolder).To(BeTrue()) + Expect(netConfigList.Plugins).To(HaveLen(1)) + }) + + It("will return error if `loadPluginsFromFolder` is true but no plugins subfolder with network name exists", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": true + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + subDir := filepath.Join(configDir, "some-network") + _, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).To(MatchError(fmt.Sprintf("no plugin config found in %s", subDir))) + }) + + It("will return error if `loadPluginsFromFolder` is true and network name subfolder exists, but no plugin configs", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": true + }`) + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + subDir := filepath.Join(configDir, "some-network") + Expect(os.MkdirAll(subDir, 0o700)).To(Succeed()) + + _, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).To(MatchError(fmt.Sprintf("no plugin config found in %s", subDir))) + }) + + It("will merge loaded and inlined plugin lists if both `plugins` is set and `loadPluginsFromFolder` is true", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "loadPluginsFromFolder": true, + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + } + ] + }`) + + dirPluginConf := []byte(`{ + "type": "bro-check-out-my-plugin", + "subnet": "10.0.0.1/24" + }`) + + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + + subDir := filepath.Join(configDir, "some-network") + Expect(os.MkdirAll(subDir, 0o700)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed()) + + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList.LoadPluginsFromFolder).To(BeTrue()) + Expect(netConfigList.Plugins).To(HaveLen(2)) + }) + + It("will ignore loaded plugins if `plugins` is set and `loadPluginsFromFolder` is not present", func() { + configList = []byte(`{ + "name": "some-network", + "cniVersion": "0.4.0", + "plugins": [ + { + "type": "host-local", + "subnet": "10.0.0.1/24" + } + ] + }`) + + dirPluginConf := []byte(`{ + "type": "bro-check-out-my-plugin", + "subnet": "10.0.0.1/24" + }`) + + Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed()) + subDir := filepath.Join(configDir, "some-network") + Expect(os.MkdirAll(subDir, 0o700)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed()) + + netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network") + Expect(err).NotTo(HaveOccurred()) + Expect(netConfigList.LoadPluginsFromFolder).To(BeFalse()) + Expect(netConfigList.Plugins).To(HaveLen(1)) + Expect(netConfigList.Plugins[0].Network.Type).To(Equal("host-local")) + }) + }) }) - Describe("ConfListFromFile", func() { + Describe("NetworkConfFromFile", func() { Context("when the file cannot be opened", func() { It("returns a useful error", func() { - _, err := libcni.ConfListFromFile("/tmp/nope/not-here") + _, err := libcni.NetworkConfFromFile("/tmp/nope/not-here") Expect(err).To(MatchError(HavePrefix(`error reading /tmp/nope/not-here: open /tmp/nope/not-here`))) }) }) @@ -505,7 +693,7 @@ var _ = Describe("Loading configuration from disk", func() { }) }) -var _ = Describe("ConfListFromBytes", func() { +var _ = Describe("NetworkConfFromBytes", func() { Describe("Version selection", func() { makeConfig := func(versions ...string) []byte { // ugly fake json encoding, but whatever @@ -516,36 +704,36 @@ var _ = Describe("ConfListFromBytes", func() { return []byte(fmt.Sprintf(`{"name": "test", "cniVersions": [%s], "plugins": [{"type": "foo"}]}`, strings.Join(vs, ","))) } It("correctly selects the maximum version", func() { - conf, err := libcni.ConfListFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.0")) + conf, err := libcni.NetworkConfFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.0")) Expect(err).NotTo(HaveOccurred()) Expect(conf.CNIVersion).To(Equal("1.1.0")) }) It("selects the highest version supported by libcni", func() { - conf, err := libcni.ConfListFromBytes(makeConfig("99.0.0", "1.1.0", "0.4.0", "1.0.0")) + conf, err := libcni.NetworkConfFromBytes(makeConfig("99.0.0", "1.1.0", "0.4.0", "1.0.0")) Expect(err).NotTo(HaveOccurred()) Expect(conf.CNIVersion).To(Equal("1.1.0")) }) It("fails when invalid versions are specified", func() { - _, err := libcni.ConfListFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.f")) + _, err := libcni.NetworkConfFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.f")) Expect(err).To(HaveOccurred()) }) It("falls back to cniVersion", func() { - conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersion": "1.2.3", "plugins": [{"type": "foo"}]}`)) + conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersion": "1.2.3", "plugins": [{"type": "foo"}]}`)) Expect(err).NotTo(HaveOccurred()) Expect(conf.CNIVersion).To(Equal("1.2.3")) }) It("merges cniVersions and cniVersion", func() { - conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersion": "1.0.0", "cniVersions": ["0.1.0", "0.4.0"], "plugins": [{"type": "foo"}]}`)) + conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersion": "1.0.0", "cniVersions": ["0.1.0", "0.4.0"], "plugins": [{"type": "foo"}]}`)) Expect(err).NotTo(HaveOccurred()) Expect(conf.CNIVersion).To(Equal("1.0.0")) }) It("handles an empty cniVersions array", func() { - conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersions": [], "plugins": [{"type": "foo"}]}`)) + conf, err := libcni.NetworkConfFromBytes([]byte(`{"name": "test", "cniVersions": [], "plugins": [{"type": "foo"}]}`)) Expect(err).NotTo(HaveOccurred()) Expect(conf.CNIVersion).To(Equal("")) }) @@ -579,7 +767,7 @@ var _ = Describe("ConfListFromConf", func() { })) // Test that the json unmarshals to the same data - ncl2, err := libcni.ConfListFromBytes(bytes) + ncl2, err := libcni.NetworkConfFromBytes(bytes) Expect(err).NotTo(HaveOccurred()) ncl2.Bytes = nil ncl2.Plugins[0].Bytes = nil