Skip to content

Commit

Permalink
Merge pull request #1010 from squeed/cni-versions
Browse files Browse the repository at this point in the history
spec, libcni: add cniVersions field in CNI configurations
  • Loading branch information
squeed committed Dec 11, 2023
2 parents d2bbac8 + a6a9891 commit 2317778
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 2 deletions.
16 changes: 14 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Configuration format](#configuration-format)
- [Plugin configuration objects:](#plugin-configuration-objects)
- [Example configuration](#example-configuration)
- [Version considerations](#version-considerations)
- [Section 2: Execution Protocol](#section-2-execution-protocol)
- [Overview](#overview-1)
- [Parameters](#parameters)
Expand Down Expand Up @@ -107,6 +108,7 @@ require this.
A network configuration consists of a JSON object with the following keys:

- `cniVersion` (string): [Semantic Version 2.0](https://semver.org) of CNI specification to which this configuration list and all the individual configurations conform. Currently "1.1.0"
- `cniVersions` (string list): List of all CNI versions which this configuration supports. See [version selection](#version-selection) below.
- `name` (string): Network name. This should be unique across all network configurations on a host (or other administrative domain). Must start with an alphanumeric character, optionally followed by any combination of one or more alphanumeric characters, underscore, dot (.) or hyphen (-).
- `disableCheck` (boolean): Either `true` or `false`. If `disableCheck` is `true`, runtimes must not call `CHECK` for this network configuration list. This allows an administrator to prevent `CHECK`ing where a combination of plugins is known to return spurious errors.
- `plugins` (list): A list of CNI plugins and their configuration, which is a list of plugin configuration objects.
Expand Down Expand Up @@ -147,6 +149,7 @@ Plugins may define additional fields that they accept and may generate an error
```jsonc
{
"cniVersion": "1.1.0",
"cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"],
"name": "dbnet",
"plugins": [
{
Expand Down Expand Up @@ -185,6 +188,15 @@ Plugins may define additional fields that they accept and may generate an error
}
```

### Version considerations

CNI runtimes, plugins, and network configurations may support multiple CNI specification versions independently. Plugins indicate their set of supported versions through the VERSION command, while network configurations indicate their set of supported versions through the `cniVersion` and `cniVersions` fields.

CNI runtimes MUST select the highest supported version from the set of network configuration versions given by the `cniVersion` and `cniVersions` fields. Runtimes MAY consider the set of supported plugin versions as reported by the VERSION command when determining available versions.


The CNI protocol follows Semantic Versioning principles, so the configuration format MUST remain backwards and forwards compatible within major versions.

## Section 2: Execution Protocol

### Overview
Expand Down Expand Up @@ -471,7 +483,7 @@ The network configuration format (which is a list of plugin configurations to ex
The request configuration for a single plugin invocation is also JSON. It consists of the plugin configuration, primarily unchanged except for the specified additions and removals.

The following fields are always to be inserted into the request configuration by the runtime:
- `cniVersion`: taken from the `cniVersion` field of the network configuration
- `cniVersion`: the protocol version selected by the runtime - the string "1.1.0"
- `name`: taken from the `name` field of the network configuration


Expand Down Expand Up @@ -596,7 +608,7 @@ Example:

Plugins should output a JSON object with the following keys if they encounter an error:

- `cniVersion`: The same value as provided by the configuration
- `cniVersion`: The protocol version in use - "1.1.0"
- `code`: A numeric error code, see below for reserved codes.
- `msg`: A short message characterizing the error.
- `details`: A longer message describing the error.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/containernetworking/cni
go 1.18

require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/onsi/ginkgo/v2 v2.13.2
github.com/onsi/gomega v1.30.0
github.com/vishvananda/netns v0.0.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
Expand Down
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 2317778

Please sign in to comment.