Skip to content

Commit a6a9891

Browse files
committed
libcni: implement version negotiation
This implements the `cniVersions` field in the network configuration list. It allows for CNI plugins to specify that they support multiple versions, and the runtime may select the highest version it supports. Signed-off-by: Casey Callendrello <[email protected]>
1 parent 1362169 commit a6a9891

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

libcni/conf.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import (
2323
"sort"
2424
"strings"
2525

26+
"github.com/Masterminds/semver/v3"
27+
2628
"github.com/containernetworking/cni/pkg/types"
29+
"github.com/containernetworking/cni/pkg/version"
2730
)
2831

2932
type NotFoundError struct {
@@ -86,6 +89,47 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) {
8689
}
8790
}
8891

92+
rawVersions, ok := rawList["cniVersions"]
93+
if ok {
94+
// Parse the current package CNI version
95+
currentVersion, err := semver.NewVersion(version.Current())
96+
if err != nil {
97+
panic("CNI version is invalid semver!")
98+
}
99+
100+
rvs, ok := rawVersions.([]interface{})
101+
if !ok {
102+
return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs)
103+
}
104+
vs := make([]*semver.Version, 0, len(rvs))
105+
for i, rv := range rvs {
106+
v, ok := rv.(string)
107+
if !ok {
108+
return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions index %d: %T", i, rv)
109+
}
110+
if v, err := semver.NewVersion(v); err != nil {
111+
return nil, fmt.Errorf("error parsing configuration list: invalid cniVersions entry %s at index %d: %w", v, i, err)
112+
} else if !v.GreaterThan(currentVersion) {
113+
// Skip versions "greater" than this implementation of the spec
114+
vs = append(vs, v)
115+
}
116+
}
117+
118+
// if cniVersion was already set, append it to the list for sorting.
119+
if cniVersion != "" {
120+
if v, err := semver.NewVersion(cniVersion); err != nil {
121+
return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion %s: %w", cniVersion, err)
122+
} else if !v.GreaterThan(currentVersion) {
123+
// ignore any versions higher than the current implemented spec version
124+
vs = append(vs, v)
125+
}
126+
}
127+
sort.Sort(semver.Collection(vs))
128+
if len(vs) > 0 {
129+
cniVersion = vs[len(vs)-1].String()
130+
}
131+
}
132+
89133
disableCheck := false
90134
if rawDisableCheck, ok := rawList["disableCheck"]; ok {
91135
disableCheck, ok = rawDisableCheck.(bool)

libcni/conf_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"os"
2020
"path/filepath"
21+
"strings"
2122

2223
. "github.com/onsi/ginkgo/v2"
2324
. "github.com/onsi/gomega"
@@ -504,6 +505,53 @@ var _ = Describe("Loading configuration from disk", func() {
504505
})
505506
})
506507

508+
var _ = Describe("ConfListFromBytes", func() {
509+
Describe("Version selection", func() {
510+
makeConfig := func(versions ...string) []byte {
511+
// ugly fake json encoding, but whatever
512+
vs := []string{}
513+
for _, v := range versions {
514+
vs = append(vs, fmt.Sprintf(`"%s"`, v))
515+
}
516+
return []byte(fmt.Sprintf(`{"name": "test", "cniVersions": [%s], "plugins": [{"type": "foo"}]}`, strings.Join(vs, ",")))
517+
}
518+
It("correctly selects the maximum version", func() {
519+
conf, err := libcni.ConfListFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.0"))
520+
Expect(err).NotTo(HaveOccurred())
521+
Expect(conf.CNIVersion).To(Equal("1.1.0"))
522+
})
523+
524+
It("selects the highest version supported by libcni", func() {
525+
conf, err := libcni.ConfListFromBytes(makeConfig("99.0.0", "1.1.0", "0.4.0", "1.0.0"))
526+
Expect(err).NotTo(HaveOccurred())
527+
Expect(conf.CNIVersion).To(Equal("1.1.0"))
528+
})
529+
530+
It("fails when invalid versions are specified", func() {
531+
_, err := libcni.ConfListFromBytes(makeConfig("1.1.0", "0.4.0", "1.0.f"))
532+
Expect(err).To(HaveOccurred())
533+
})
534+
535+
It("falls back to cniVersion", func() {
536+
conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersion": "1.2.3", "plugins": [{"type": "foo"}]}`))
537+
Expect(err).NotTo(HaveOccurred())
538+
Expect(conf.CNIVersion).To(Equal("1.2.3"))
539+
})
540+
541+
It("merges cniVersions and cniVersion", func() {
542+
conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersion": "1.0.0", "cniVersions": ["0.1.0", "0.4.0"], "plugins": [{"type": "foo"}]}`))
543+
Expect(err).NotTo(HaveOccurred())
544+
Expect(conf.CNIVersion).To(Equal("1.0.0"))
545+
})
546+
547+
It("handles an empty cniVersions array", func() {
548+
conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersions": [], "plugins": [{"type": "foo"}]}`))
549+
Expect(err).NotTo(HaveOccurred())
550+
Expect(conf.CNIVersion).To(Equal(""))
551+
})
552+
})
553+
})
554+
507555
var _ = Describe("ConfListFromConf", func() {
508556
var testNetConfig *libcni.NetworkConfig
509557

0 commit comments

Comments
 (0)