Skip to content

Commit

Permalink
libcni: implement version negotiation
Browse files Browse the repository at this point in the history
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 <c1@caseyc.net>
  • Loading branch information
squeed committed Dec 11, 2023
1 parent 1362169 commit a6a9891
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 0 deletions.
44 changes: 44 additions & 0 deletions libcni/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -86,6 +89,47 @@ 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))
if len(vs) > 0 {
cniVersion = vs[len(vs)-1].String()
}
}

disableCheck := false
if rawDisableCheck, ok := rawList["disableCheck"]; ok {
disableCheck, ok = rawDisableCheck.(bool)
Expand Down
48 changes: 48 additions & 0 deletions libcni/conf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -504,6 +505,53 @@ 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"))
})

It("handles an empty cniVersions array", func() {
conf, err := libcni.ConfListFromBytes([]byte(`{"name": "test", "cniVersions": [], "plugins": [{"type": "foo"}]}`))
Expect(err).NotTo(HaveOccurred())
Expect(conf.CNIVersion).To(Equal(""))
})
})
})

var _ = Describe("ConfListFromConf", func() {
var testNetConfig *libcni.NetworkConfig

Expand Down

0 comments on commit a6a9891

Please sign in to comment.