Skip to content

Commit 2d76833

Browse files
committed
libcni: Support subdirectory-based plugin loading (#928)
Signed-off-by: Benjamin Leggett <[email protected]>
1 parent 45eac09 commit 2d76833

File tree

3 files changed

+288
-13
lines changed

3 files changed

+288
-13
lines changed

Diff for: libcni/api.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ type PluginConfig struct {
7777
}
7878

7979
type NetworkConfigList struct {
80-
Name string
81-
CNIVersion string
82-
DisableCheck bool
80+
Name string
81+
CNIVersion string
82+
DisableCheck bool
8383
LoadOnlyInlinedPlugins bool
84-
Plugins []*PluginConfig
85-
Bytes []byte
84+
Plugins []*PluginConfig
85+
Bytes []byte
8686
}
8787

8888
type NetworkAttachment struct {

Diff for: libcni/conf.go

+74-8
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,32 @@ func NetworkPluginConfFromBytes(pluginConfBytes []byte) (*PluginConfig, error) {
6464
return conf, nil
6565
}
6666

67+
// Given a path to a directory containing a network configuration, and the name of a network,
68+
// loads all plugin definitions found at path `networkConfPath/networkName/*.conf`
69+
func NetworkPluginConfsFromFiles(networkConfPath, networkName string) ([]*PluginConfig, error) {
70+
var pConfs []*PluginConfig
71+
72+
pluginConfPath := filepath.Join(networkConfPath, networkName)
73+
74+
pluginConfFiles, err := ConfFiles(pluginConfPath, []string{".conf"})
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to read plugin config files in %s: %w", pluginConfPath, err)
77+
}
78+
79+
for _, pluginConfFile := range pluginConfFiles {
80+
pluginConfBytes, err := os.ReadFile(pluginConfFile)
81+
if err != nil {
82+
return nil, fmt.Errorf("error reading %s: %w", pluginConfFile, err)
83+
}
84+
pluginConf, err := NetworkPluginConfFromBytes(pluginConfBytes)
85+
if err != nil {
86+
return nil, err
87+
}
88+
pConfs = append(pConfs, pluginConf)
89+
}
90+
return pConfs, nil
91+
}
92+
6793
func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
6894
rawList := make(map[string]interface{})
6995
if err := json.Unmarshal(confBytes, &rawList); err != nil {
@@ -148,18 +174,38 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
148174
}
149175
}
150176

177+
loadOnlyInlinedPlugins := false
178+
if rawLoadCheck, ok := rawList["loadOnlyInlinedPlugins"]; ok {
179+
loadOnlyInlinedPlugins, ok = rawLoadCheck.(bool)
180+
if !ok {
181+
return nil, fmt.Errorf("error parsing configuration list: invalid loadOnlyInlinedPlugins type %T", rawLoadCheck)
182+
}
183+
}
184+
151185
list := &NetworkConfigList{
152-
Name: name,
153-
DisableCheck: disableCheck,
154-
CNIVersion: cniVersion,
155-
Bytes: confBytes,
186+
Name: name,
187+
DisableCheck: disableCheck,
188+
LoadOnlyInlinedPlugins: loadOnlyInlinedPlugins,
189+
CNIVersion: cniVersion,
190+
Bytes: confBytes,
156191
}
157192

158193
var plugins []interface{}
159194
plug, ok := rawList["plugins"]
160-
if !ok {
161-
return nil, fmt.Errorf("error parsing configuration list: no 'plugins' key")
195+
// We can have a `plugins` list key in the main conf,
196+
// We can also have `loadOnlyInlinedPlugins == true`
197+
//
198+
// If `plugins` is there, then `loadOnlyInlinedPlugins` can be true
199+
//
200+
// If plugins is NOT there, then `loadOnlyInlinedPlugins` cannot be true
201+
//
202+
// We have to have at least some plugins.
203+
if !ok && loadOnlyInlinedPlugins {
204+
return nil, fmt.Errorf("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key")
205+
} else if !ok && !loadOnlyInlinedPlugins {
206+
return list, nil
162207
}
208+
163209
plugins, ok = plug.([]interface{})
164210
if !ok {
165211
return nil, fmt.Errorf("error parsing configuration list: invalid 'plugins' type %T", plug)
@@ -179,7 +225,6 @@ func NetworkConfFromBytes(confBytes []byte) (*NetworkConfigList, error) {
179225
}
180226
list.Plugins = append(list.Plugins, netConf)
181227
}
182-
183228
return list, nil
184229
}
185230

@@ -188,7 +233,26 @@ func NetworkConfFromFile(filename string) (*NetworkConfigList, error) {
188233
if err != nil {
189234
return nil, fmt.Errorf("error reading %s: %w", filename, err)
190235
}
191-
return ConfListFromBytes(bytes)
236+
237+
conf, err := NetworkConfFromBytes(bytes)
238+
if err != nil {
239+
return nil, err
240+
}
241+
242+
if !conf.LoadOnlyInlinedPlugins {
243+
plugins, err := NetworkPluginConfsFromFiles(filepath.Dir(filename), conf.Name)
244+
if err != nil {
245+
return nil, err
246+
}
247+
conf.Plugins = append(conf.Plugins, plugins...)
248+
}
249+
250+
if len(conf.Plugins) == 0 {
251+
// Having 0 plugins for a given network is not necessarily a problem,
252+
// but return as error for caller to decide, since they tried to load
253+
return nil, fmt.Errorf("no plugin configs found")
254+
}
255+
return conf, nil
192256
}
193257

194258
// Deprecated: This file format is no longer supported, use NetworkConfXXX and NetworkPluginXXX functions
@@ -223,6 +287,8 @@ func ConfFiles(dir string, extensions []string) ([]string, error) {
223287
switch {
224288
case err == nil: // break
225289
case os.IsNotExist(err):
290+
// If folder not there, return no error - only return an
291+
// error if we cannot read contents or there are no contents.
226292
return nil, nil
227293
default:
228294
return nil, err

Diff for: libcni/conf_test.go

+209
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,215 @@ var _ = Describe("Loading configuration from disk", func() {
389389
Expect(err).To(MatchError(fmt.Sprintf("error parsing configuration list: invalid disableCheck value \"%s\"", badValue)))
390390
})
391391
})
392+
393+
Context("for loadOnlyInlinedPlugins", func() {
394+
It("the value will be parsed", func() {
395+
configList = []byte(`{
396+
"name": "some-network",
397+
"cniVersion": "0.4.0",
398+
"loadOnlyInlinedPlugins": true,
399+
"plugins": [
400+
{
401+
"type": "host-local",
402+
"subnet": "10.0.0.1/24"
403+
}
404+
]
405+
}`)
406+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
407+
408+
dirPluginConf := []byte(`{
409+
"type": "bro-check-out-my-plugin",
410+
"subnet": "10.0.0.1/24"
411+
}`)
412+
413+
subDir := filepath.Join(configDir, "some-network")
414+
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
415+
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
416+
417+
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
418+
Expect(err).NotTo(HaveOccurred())
419+
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeTrue())
420+
})
421+
422+
It("the value will be false if not in config", func() {
423+
configList = []byte(`{
424+
"name": "some-network",
425+
"cniVersion": "0.4.0",
426+
"plugins": [
427+
{
428+
"type": "host-local",
429+
"subnet": "10.0.0.1/24"
430+
}
431+
]
432+
}`)
433+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
434+
435+
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
436+
Expect(err).NotTo(HaveOccurred())
437+
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
438+
})
439+
440+
It("will return an error on an unrecognized value", func() {
441+
const badValue string = "sphagnum"
442+
configList = []byte(fmt.Sprintf(`{
443+
"name": "some-network",
444+
"cniVersion": "0.4.0",
445+
"loadOnlyInlinedPlugins": "%s",
446+
"plugins": [
447+
{
448+
"type": "host-local",
449+
"subnet": "10.0.0.1/24"
450+
}
451+
]
452+
}`, badValue))
453+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
454+
455+
_, err := libcni.LoadNetworkConf(configDir, "some-network")
456+
Expect(err).To(MatchError("error parsing configuration list: invalid loadOnlyInlinedPlugins type string"))
457+
})
458+
459+
It("will return an error if `plugins` is missing and `loadOnlyInlinedPlugins` is `true`", func() {
460+
configList = []byte(`{
461+
"name": "some-network",
462+
"cniVersion": "0.4.0",
463+
"loadOnlyInlinedPlugins": true
464+
}`)
465+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
466+
467+
_, err := libcni.LoadNetworkConf(configDir, "some-network")
468+
Expect(err).To(MatchError("error parsing configuration list: `loadOnlyInlinedPlugins` is true, and no 'plugins' key"))
469+
})
470+
471+
It("will return no error if `plugins` is missing and `loadOnlyInlinedPlugins` is false", func() {
472+
configList = []byte(`{
473+
"name": "some-network",
474+
"cniVersion": "0.4.0",
475+
"loadOnlyInlinedPlugins": false
476+
}`)
477+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
478+
479+
dirPluginConf := []byte(`{
480+
"type": "bro-check-out-my-plugin",
481+
"subnet": "10.0.0.1/24"
482+
}`)
483+
484+
subDir := filepath.Join(configDir, "some-network")
485+
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
486+
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
487+
488+
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
489+
Expect(err).NotTo(HaveOccurred())
490+
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
491+
Expect(netConfigList.Plugins).To(HaveLen(1))
492+
})
493+
494+
It("will return error if `loadOnlyInlinedPlugins` is implicitly false + no conf plugin is defined, but no plugins subfolder with network name exists", func() {
495+
configList = []byte(`{
496+
"name": "some-network",
497+
"cniVersion": "0.4.0"
498+
}`)
499+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
500+
501+
_, err := libcni.LoadNetworkConf(configDir, "some-network")
502+
Expect(err).To(MatchError("no plugin configs found"))
503+
})
504+
505+
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() {
506+
configList = []byte(`{
507+
"name": "some-network",
508+
"cniVersion": "0.4.0",
509+
"plugins": [
510+
{
511+
"type": "host-local",
512+
"subnet": "10.0.0.1/24"
513+
}
514+
]
515+
}`)
516+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
517+
518+
_, err := libcni.LoadNetworkConf(configDir, "some-network")
519+
Expect(err).NotTo(HaveOccurred())
520+
})
521+
522+
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() {
523+
configList = []byte(`{
524+
"name": "some-network",
525+
"cniVersion": "0.4.0",
526+
"plugins": [
527+
{
528+
"type": "host-local",
529+
"subnet": "10.0.0.1/24"
530+
}
531+
]
532+
}`)
533+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
534+
535+
subDir := filepath.Join(configDir, "some-network")
536+
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
537+
538+
_, err := libcni.LoadNetworkConf(configDir, "some-network")
539+
Expect(err).NotTo(HaveOccurred())
540+
})
541+
542+
It("will merge loaded and inlined plugin lists if both `plugins` is set and `loadOnlyInlinedPlugins` is false", func() {
543+
configList = []byte(`{
544+
"name": "some-network",
545+
"cniVersion": "0.4.0",
546+
"plugins": [
547+
{
548+
"type": "host-local",
549+
"subnet": "10.0.0.1/24"
550+
}
551+
]
552+
}`)
553+
554+
dirPluginConf := []byte(`{
555+
"type": "bro-check-out-my-plugin",
556+
"subnet": "10.0.0.1/24"
557+
}`)
558+
559+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
560+
561+
subDir := filepath.Join(configDir, "some-network")
562+
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
563+
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
564+
565+
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
566+
Expect(err).NotTo(HaveOccurred())
567+
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeFalse())
568+
Expect(netConfigList.Plugins).To(HaveLen(2))
569+
})
570+
571+
It("will ignore loaded plugins if `plugins` is set and `loadOnlyInlinedPlugins` is true", func() {
572+
configList = []byte(`{
573+
"name": "some-network",
574+
"cniVersion": "0.4.0",
575+
"loadOnlyInlinedPlugins": true,
576+
"plugins": [
577+
{
578+
"type": "host-local",
579+
"subnet": "10.0.0.1/24"
580+
}
581+
]
582+
}`)
583+
584+
dirPluginConf := []byte(`{
585+
"type": "bro-check-out-my-plugin",
586+
"subnet": "10.0.0.1/24"
587+
}`)
588+
589+
Expect(os.WriteFile(filepath.Join(configDir, "50-whatever.conflist"), configList, 0o600)).To(Succeed())
590+
subDir := filepath.Join(configDir, "some-network")
591+
Expect(os.MkdirAll(subDir, 0o700)).To(Succeed())
592+
Expect(os.WriteFile(filepath.Join(subDir, "funky-second-plugin.conf"), dirPluginConf, 0o600)).To(Succeed())
593+
594+
netConfigList, err := libcni.LoadNetworkConf(configDir, "some-network")
595+
Expect(err).NotTo(HaveOccurred())
596+
Expect(netConfigList.LoadOnlyInlinedPlugins).To(BeTrue())
597+
Expect(netConfigList.Plugins).To(HaveLen(1))
598+
Expect(netConfigList.Plugins[0].Network.Type).To(Equal("host-local"))
599+
})
600+
})
392601
})
393602

394603
Describe("NetworkConfFromFile", func() {

0 commit comments

Comments
 (0)