Skip to content

Commit

Permalink
libcni: Support subdirectory-based plugin loading (#928)
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Leggett <[email protected]>
  • Loading branch information
bleggett committed Apr 8, 2024
1 parent 5d96e63 commit 9a1d54e
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 8 deletions.
82 changes: 74 additions & 8 deletions libcni/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,32 @@ func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) {
return conf, nil
}

// 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) {
var pConfs []*PluginConfig

pluginConfPath := filepath.Join(networkConfPath, networkName)

pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"})
if err != nil {
return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err)
}

for _, pluginConfFile := range pluginConfFiles {
pluginConfBytes, err := os.ReadFile(pluginConfFile)
if err != nil {
return nil, fmt.Errorf("error reading %s: %w", pluginConfFile, err)
}
pluginConf, err := NetworkPluginConfFromBytes(pluginConfBytes)
if err != nil {
return nil, err
}
pConfs = append(pConfs, pluginConf)
}
return pConfs, nil
}

func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
rawList := make(map[string]interface{})
if err := json.Unmarshal(confBytes, &rawList); err != nil {
Expand Down Expand Up @@ -148,18 +174,38 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
}
}

loadOnlyInlinedPlugins := false
if rawLoadCheck, ok := rawList["loadOnlyInlinedPlugins"]; ok {
loadOnlyInlinedPlugins, ok = rawLoadCheck.(bool)
if !ok {
return nil, fmt.Errorf("error parsing configuration list: invalid loadOnlyInlinedPlugins type %T", rawLoadCheck)
}
}

list := &NetworkConfigList{
Name: name,
DisableCheck: disableCheck,
CNIVersion: cniVersion,
Bytes: confBytes,
Name: name,
DisableCheck: disableCheck,
LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins,
CNIVersion: cniVersion,
Bytes: confBytes,
}

var plugins []interface{}
plug, ok := rawList["plugins"]
if !ok {
return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
// We can have a `plugins` list key in the main conf,
// We can also have `loadOnlyInlinedPlugins == true`
//
// If `plugins` is there, then `loadOnlyInlinedPlugins` can be true
//
// If plugins is NOT there, then `loadOnlyInlinedPlugins` cannot be true
//
// We have to have at least some plugins.
if !ok && loadOnlyInlinedPlugins {
return nil, fmt.Errorf("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key")
} else if !ok && !loadOnlyInlinedPlugins {
return list, nil
}

plugins, ok = plug.([]interface{})
if !ok {
return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
Expand All @@ -179,7 +225,6 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
}
list.Plugins = append(list.Plugins, netConf)
}

return list, nil
}

Expand All @@ -188,7 +233,26 @@ func NetworkConfFromFile(filename string) (*NetworkConfigList, error) {
if err != nil {
return nil, fmt.Errorf("error reading %s: %w", filename, err)
}
return ConfListFromBytes(bytes)

conf, err := NetworkConfFromBytes(bytes)
if err != nil {
return nil, err
}

if !conf.LoadOnlyInlinedPlugins {
plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name)
if err != nil {
return nil, err
}
conf.Plugins = append(conf.Plugins, plugins...)
}

if len(conf.Plugins) == 0 {
// Having 0 plugins for a given network is not necessarily a problem,
// but return as error for caller to decide, since they tried to load
return nil, fmt.Errorf("no plugin configs found")
}
return conf, nil
}

// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
Expand Down Expand Up @@ -223,6 +287,8 @@ func ConfFiles(dir string, extensions []string) ([]string, error) {
switch {
case err == nil: // break
case os.IsNotExist(err):
// If folder not there, return no error - only return an
// error if we cannot read contents or there are no contents.
return nil, nil
default:
return nil, err
Expand Down
209 changes: 209 additions & 0 deletions libcni/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,215 @@ var _ = Describe("Loading configuration from disk", func() {
Expect(err).To(MatchError(fmt.Sprintf("error parsing configuration list: invalid disableCheck value \"%s\"", badValue)))
})
})

Context("for loadOnlyInlinedPlugins", func() {
It("the value will be parsed", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": 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.LoadOnlyInlinedPlugins).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.LoadOnlyInlinedPlugins).To(BeFalse())
})

It("will return an error on an unrecognized value", func() {
const badValue string = "sphagnum"
configList = []byte(fmt.Sprintf(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": "%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 loadOnlyInlinedPlugins type string"))
})

It("will return an error if `plugins` is missing and `loadOnlyInlinedPlugins` is `true`", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": true
}`)
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: `loadOnlyInlinedPlugins` is true, and no 'plugins' key"))
})

It("will return no error if `plugins` is missing and `loadOnlyInlinedPlugins` is false", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": false
}`)
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.LoadOnlyInlinedPlugins).To(BeFalse())
Expect(netConfigList.Plugins).To(HaveLen(1))
})

It("will return error if `loadOnlyInlinedPlugins` is implicitly false + no conf plugin is defined, but no plugins subfolder with network name exists", 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("no plugin configs found"))
})

It("will return NO error if `loadOnlyInlinedPlugins` is implicitly false + at least 1 conf plugin is defined, but no plugins subfolder with network name exists", 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())

_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
})

It("will return NO error if `loadOnlyInlinedPlugins` is implicitly false + at least 1 conf plugin is defined and network name subfolder exists, but is empty/unreadable", 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())

subDir := filepath.Join(configDir, "some-network")
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())

_, err := libcni.LoadNetworkConf(configDir, "some-network")
Expect(err).NotTo(HaveOccurred())
})

It("will merge loaded and inlined plugin lists if both `plugins` is set and `loadOnlyInlinedPlugins` is false", 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.LoadOnlyInlinedPlugins).To(BeFalse())
Expect(netConfigList.Plugins).To(HaveLen(2))
})

It("will ignore loaded plugins if `plugins` is set and `loadOnlyInlinedPlugins` is true", func() {
configList = []byte(`{
"name": "some-network",
"cniVersion": "0.4.0",
"loadOnlyInlinedPlugins": 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.LoadOnlyInlinedPlugins).To(BeTrue())
Expect(netConfigList.Plugins).To(HaveLen(1))
Expect(netConfigList.Plugins[0].Network.Type).To(Equal("host-local"))
})
})
})

Describe("NetworkConfFromFile", func() {
Expand Down

0 comments on commit 9a1d54e

Please sign in to comment.