From afa391ace889744cd8b006180b6dacca39d12a3e Mon Sep 17 00:00:00 2001 From: Casey Callendrello Date: Wed, 1 Mar 2023 17:46:19 +0100 Subject: [PATCH] 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 --- libcni/conf.go | 42 ++++++++++++++++++++++++++++++++++++++++++ libcni/conf_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/libcni/conf.go b/libcni/conf.go index 4be29311..c33a4d68 100644 --- a/libcni/conf.go +++ b/libcni/conf.go @@ -23,7 +23,10 @@ import ( "sort" "strings" + "github.com/Masterminds/semver/v3" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" ) type NotFoundError struct { @@ -86,6 +89,45 @@ func ConfListFromBytes(bytes []byte) (*NetworkConfigList, error) { } } + rawVersions, ok := rawList["cniVersions"] + if ok { + // Parse the current package CNI version + currentVersion, err := semver.NewVersion(version.Current()) + if err != nil { + panic("CNI version is invalid semver!") + } + + rvs, ok := rawVersions.([]interface{}) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions: %T", rvs) + } + vs := make([]*semver.Version, 0, len(rvs)) + for i, rv := range rvs { + v, ok := rv.(string) + if !ok { + return nil, fmt.Errorf("error parsing configuration list: invalid type for cniVersions index %d: %T", i, rv) + } + if v, err := semver.NewVersion(v); err != nil { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersions entry %s at index %d: %w", v, i, err) + } else if !v.GreaterThan(currentVersion) { + // Skip versions "greater" than this implementation of the spec + vs = append(vs, v) + } + } + + // if cniVersion was already set, append it to the list for sorting. + if cniVersion != "" { + if v, err := semver.NewVersion(cniVersion); err != nil { + return nil, fmt.Errorf("error parsing configuration list: invalid cniVersion %s: %w", cniVersion, err) + } else if !v.GreaterThan(currentVersion) { + // ignore any versions higher than the current implemented spec version + vs = append(vs, v) + } + } + sort.Sort(semver.Collection(vs)) + cniVersion = vs[len(vs)-1].String() + } + disableCheck := false if rawDisableCheck, ok := rawList["disableCheck"]; ok { disableCheck, ok = rawDisableCheck.(bool) diff --git a/libcni/conf_test.go b/libcni/conf_test.go index 74fb0dc2..41488046 100644 --- a/libcni/conf_test.go +++ b/libcni/conf_test.go @@ -18,6 +18,7 @@ import ( "fmt" "os" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -504,6 +505,47 @@ var _ = Describe("Loading configuration from disk", func() { }) }) +var _ = Describe("ConfListFromBytes", func() { + Describe("Version selection", func() { + makeConfig := func(versions ...string) []byte { + // ugly fake json encoding, but whatever + vs := []string{} + for _, v := range versions { + vs = append(vs, fmt.Sprintf(`"%s"`, v)) + } + 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")) + 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")) + 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")) + Expect(err).To(HaveOccurred()) + }) + + It("falls back to cniVersion", func() { + conf, err := libcni.ConfListFromBytes([]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"}]}`)) + Expect(err).NotTo(HaveOccurred()) + Expect(conf.CNIVersion).To(Equal("1.0.0")) + }) + }) +}) + var _ = Describe("ConfListFromConf", func() { var testNetConfig *libcni.NetworkConfig