Skip to content

Commit 2317778

Browse files
authored
Merge pull request #1010 from squeed/cni-versions
spec, libcni: add cniVersions field in CNI configurations
2 parents d2bbac8 + a6a9891 commit 2317778

File tree

5 files changed

+109
-2
lines changed

5 files changed

+109
-2
lines changed

SPEC.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- [Configuration format](#configuration-format)
99
- [Plugin configuration objects:](#plugin-configuration-objects)
1010
- [Example configuration](#example-configuration)
11+
- [Version considerations](#version-considerations)
1112
- [Section 2: Execution Protocol](#section-2-execution-protocol)
1213
- [Overview](#overview-1)
1314
- [Parameters](#parameters)
@@ -107,6 +108,7 @@ require this.
107108
A network configuration consists of a JSON object with the following keys:
108109

109110
- `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"
111+
- `cniVersions` (string list): List of all CNI versions which this configuration supports. See [version selection](#version-selection) below.
110112
- `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 (-).
111113
- `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.
112114
- `plugins` (list): A list of CNI plugins and their configuration, which is a list of plugin configuration objects.
@@ -147,6 +149,7 @@ Plugins may define additional fields that they accept and may generate an error
147149
```jsonc
148150
{
149151
"cniVersion": "1.1.0",
152+
"cniVersions": ["0.3.1", "0.4.0", "1.0.0", "1.1.0"],
150153
"name": "dbnet",
151154
"plugins": [
152155
{
@@ -185,6 +188,15 @@ Plugins may define additional fields that they accept and may generate an error
185188
}
186189
```
187190

191+
### Version considerations
192+
193+
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.
194+
195+
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.
196+
197+
198+
The CNI protocol follows Semantic Versioning principles, so the configuration format MUST remain backwards and forwards compatible within major versions.
199+
188200
## Section 2: Execution Protocol
189201

190202
### Overview
@@ -471,7 +483,7 @@ The network configuration format (which is a list of plugin configurations to ex
471483
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.
472484

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

477489

@@ -596,7 +608,7 @@ Example:
596608

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

599-
- `cniVersion`: The same value as provided by the configuration
611+
- `cniVersion`: The protocol version in use - "1.1.0"
600612
- `code`: A numeric error code, see below for reserved codes.
601613
- `msg`: A short message characterizing the error.
602614
- `details`: A longer message describing the error.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/containernetworking/cni
33
go 1.18
44

55
require (
6+
github.com/Masterminds/semver/v3 v3.2.1
67
github.com/onsi/ginkgo/v2 v2.13.2
78
github.com/onsi/gomega v1.30.0
89
github.com/vishvananda/netns v0.0.4

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
2+
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
13
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
24
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
35
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=

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)