Skip to content

Commit 5b3b5d8

Browse files
authored
Merge pull request #93 from elezar/min-spec-version
Add functions to query and validate minimum spec version
2 parents e93a674 + 1a1203c commit 5b3b5d8

File tree

7 files changed

+302
-16
lines changed

7 files changed

+302
-16
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/spf13/cobra v1.6.0
1111
github.com/stretchr/testify v1.7.0
1212
github.com/xeipuuv/gojsonschema v1.2.0
13+
golang.org/x/mod v0.4.2
1314
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c
1415
sigs.k8s.io/yaml v1.3.0
1516
)

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,16 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
7272
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
7373
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
7474
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
75+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
76+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
77+
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
78+
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
79+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
80+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
7581
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
82+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
83+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
84+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7685
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7786
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7887
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -84,8 +93,12 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
8493
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c h1:DHcbWVXeY+0Y8HHKR+rbLwnoh2F4tNCY7rTiHJ30RmA=
8594
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8695
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
96+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8797
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
8898
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
99+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
100+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
101+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
89102
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
90103
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
91104
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=

pkg/cdi/container-edits.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ func sortMounts(specgen *ocigen.Generator) {
298298
// orderedMounts defines how to sort an OCI Spec Mount slice.
299299
// This is the almost the same implementation sa used by CRI-O and Docker,
300300
// with a minor tweak for stable sorting order (easier to test):
301-
// https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26
301+
//
302+
// https://github.com/moby/moby/blob/17.05.x/daemon/volumes.go#L26
302303
type orderedMounts []oci.Mount
303304

304305
// Len returns the number of mounts. Used in sorting.

pkg/cdi/regressions_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestCDIInjectionRace(t *testing.T) {
4343
{description: "expect properly injected resolvable CDI devices",
4444
cdiSpecFiles: []string{
4545
`
46-
cdiVersion: "0.2.0"
46+
cdiVersion: "0.3.0"
4747
kind: "vendor1.com/device"
4848
devices:
4949
- name: foo
@@ -60,7 +60,7 @@ containerEdits:
6060
- "VENDOR1=present"
6161
`,
6262
`
63-
cdiVersion: "0.2.0"
63+
cdiVersion: "0.3.0"
6464
kind: "vendor2.com/device"
6565
devices:
6666
- name: bar

pkg/cdi/spec.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,11 @@ import (
3232
)
3333

3434
const (
35-
// CurrentVersion is the current vesion of the CDI Spec.
36-
CurrentVersion = cdi.CurrentVersion
37-
3835
// defaultSpecExt is the file extension for the default encoding.
3936
defaultSpecExt = ".yaml"
4037
)
4138

4239
var (
43-
// Valid CDI Spec versions.
44-
validSpecVersions = map[string]struct{}{
45-
"0.1.0": {},
46-
"0.2.0": {},
47-
"0.3.0": {},
48-
"0.4.0": {},
49-
"0.5.0": {},
50-
}
51-
5240
// Externally set CDI Spec validation function.
5341
specValidator func(*cdi.Spec) error
5442
validatorLock sync.RWMutex
@@ -216,6 +204,15 @@ func (s *Spec) validate() (map[string]*Device, error) {
216204
if err := validateVersion(s.Version); err != nil {
217205
return nil, err
218206
}
207+
208+
minVersion, err := MinimumRequiredVersion(s.Spec)
209+
if err != nil {
210+
return nil, fmt.Errorf("could not determine minumum required version: %v", err)
211+
}
212+
if newVersion(minVersion).IsGreaterThan(newVersion(s.Version)) {
213+
return nil, fmt.Errorf("the spec version must be at least v%v", minVersion)
214+
}
215+
219216
if err := ValidateVendorName(s.vendor); err != nil {
220217
return nil, err
221218
}
@@ -243,7 +240,7 @@ func (s *Spec) validate() (map[string]*Device, error) {
243240

244241
// validateVersion checks whether the specified spec version is supported.
245242
func validateVersion(version string) error {
246-
if _, ok := validSpecVersions[version]; !ok {
243+
if !validSpecVersions.isValidVersion(version) {
247244
return fmt.Errorf("invalid version %q", version)
248245
}
249246

pkg/cdi/spec_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,117 @@ func specType(content []byte) string {
527527
func TestCurrentVersionIsValid(t *testing.T) {
528528
require.NoError(t, validateVersion(cdi.CurrentVersion))
529529
}
530+
531+
func TestRequiredVersion(t *testing.T) {
532+
533+
testCases := []struct {
534+
description string
535+
spec *cdi.Spec
536+
expectedVersion string
537+
}{
538+
{
539+
description: "empty spec returns lowest version",
540+
spec: &cdi.Spec{},
541+
expectedVersion: "0.3.0",
542+
},
543+
{
544+
description: "hostPath set returns version 0.5.0",
545+
spec: &cdi.Spec{
546+
ContainerEdits: cdi.ContainerEdits{
547+
DeviceNodes: []*cdi.DeviceNode{
548+
{
549+
HostPath: "/host/path/set",
550+
},
551+
},
552+
},
553+
},
554+
expectedVersion: "0.5.0",
555+
},
556+
{
557+
description: "hostPath equal to Path required v0.5.0",
558+
spec: &cdi.Spec{
559+
ContainerEdits: cdi.ContainerEdits{
560+
DeviceNodes: []*cdi.DeviceNode{
561+
{
562+
HostPath: "/some/path",
563+
Path: "/some/path",
564+
},
565+
},
566+
},
567+
},
568+
expectedVersion: "0.5.0",
569+
},
570+
{
571+
description: "mount type set returns version 0.4.0",
572+
spec: &cdi.Spec{
573+
ContainerEdits: cdi.ContainerEdits{
574+
Mounts: []*cdi.Mount{
575+
{
576+
Type: "bind",
577+
},
578+
},
579+
},
580+
},
581+
expectedVersion: "0.4.0",
582+
},
583+
{
584+
description: "newest required version is selected",
585+
spec: &cdi.Spec{
586+
ContainerEdits: cdi.ContainerEdits{
587+
DeviceNodes: []*cdi.DeviceNode{
588+
{
589+
HostPath: "/host/path/set",
590+
},
591+
},
592+
Mounts: []*cdi.Mount{
593+
{
594+
Type: "bind",
595+
},
596+
},
597+
},
598+
},
599+
expectedVersion: "0.5.0",
600+
},
601+
{
602+
description: "device with name starting with digit requires v0.5.0",
603+
spec: &cdi.Spec{
604+
Devices: []cdi.Device{
605+
{
606+
Name: "0",
607+
ContainerEdits: cdi.ContainerEdits{
608+
Env: []string{
609+
"FOO=bar",
610+
},
611+
},
612+
},
613+
},
614+
},
615+
expectedVersion: "0.5.0",
616+
},
617+
{
618+
description: "device with name starting with letter requires minimum version",
619+
spec: &cdi.Spec{
620+
Devices: []cdi.Device{
621+
{
622+
Name: "device0",
623+
ContainerEdits: cdi.ContainerEdits{
624+
Env: []string{
625+
"FOO=bar",
626+
},
627+
},
628+
},
629+
},
630+
},
631+
expectedVersion: "0.3.0",
632+
},
633+
}
634+
635+
for _, tc := range testCases {
636+
t.Run(tc.description, func(t *testing.T) {
637+
v, err := MinimumRequiredVersion(tc.spec)
638+
require.NoError(t, err)
639+
640+
require.Equal(t, tc.expectedVersion, v)
641+
})
642+
}
643+
}

pkg/cdi/version.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright © The CDI Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cdi
18+
19+
import (
20+
"strings"
21+
22+
"golang.org/x/mod/semver"
23+
24+
cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
25+
)
26+
27+
const (
28+
// CurrentVersion is the current version of the CDI Spec.
29+
CurrentVersion = cdi.CurrentVersion
30+
31+
// vCurrent is the current version as a semver-comparable type
32+
vCurrent version = "v" + CurrentVersion
33+
34+
// These represent the released versions of the CDI specification
35+
v010 version = "v0.1.0"
36+
v020 version = "v0.2.0"
37+
v030 version = "v0.3.0"
38+
v040 version = "v0.4.0"
39+
v050 version = "v0.5.0"
40+
41+
// vEarliest is the earliest supported version of the CDI specification
42+
vEarliest version = v030
43+
)
44+
45+
// validSpecVersions stores a map of spec versions to functions to check the required versions.
46+
// Adding new fields / spec versions requires that a `requiredFunc` be implemented and
47+
// this map be updated.
48+
var validSpecVersions = requiredVersionMap{
49+
v010: nil,
50+
v020: nil,
51+
v030: nil,
52+
v040: requiresV040,
53+
v050: requiresV050,
54+
}
55+
56+
// MinimumRequiredVersion determines the minumum spec version for the input spec.
57+
func MinimumRequiredVersion(spec *cdi.Spec) (string, error) {
58+
minVersion := validSpecVersions.requiredVersion(spec)
59+
return minVersion.String(), nil
60+
}
61+
62+
// version represents a semantic version string
63+
type version string
64+
65+
// newVersion creates a version that can be used for semantic version comparisons.
66+
func newVersion(v string) version {
67+
return version("v" + strings.TrimPrefix(v, "v"))
68+
}
69+
70+
// String returns the string representation of the version.
71+
// This trims a leading v if present.
72+
func (v version) String() string {
73+
return strings.TrimPrefix(string(v), "v")
74+
}
75+
76+
// IsGreaterThan checks with a version is greater than the specified version.
77+
func (v version) IsGreaterThan(o version) bool {
78+
return semver.Compare(string(v), string(o)) > 0
79+
}
80+
81+
// IsLatest checks whether the version is the latest supported version
82+
func (v version) IsLatest() bool {
83+
return v == vCurrent
84+
}
85+
86+
type requiredFunc func(*cdi.Spec) bool
87+
88+
type requiredVersionMap map[version]requiredFunc
89+
90+
// isValidVersion checks whether the specified version is valid.
91+
// A version is valid if it is contained in the required version map.
92+
func (r requiredVersionMap) isValidVersion(specVersion string) bool {
93+
_, ok := validSpecVersions[newVersion(specVersion)]
94+
95+
return ok
96+
}
97+
98+
// requiredVersion returns the minimum version required for the given spec
99+
func (r requiredVersionMap) requiredVersion(spec *cdi.Spec) version {
100+
minVersion := vEarliest
101+
102+
for v, isRequired := range validSpecVersions {
103+
if isRequired == nil {
104+
continue
105+
}
106+
if isRequired(spec) && v.IsGreaterThan(minVersion) {
107+
minVersion = v
108+
}
109+
// If we have already detected the latest version then no later version could be detected
110+
if minVersion.IsLatest() {
111+
break
112+
}
113+
}
114+
115+
return minVersion
116+
}
117+
118+
// requiresV050 returns true if the spec uses v0.5.0 features
119+
func requiresV050(spec *cdi.Spec) bool {
120+
var edits []*cdi.ContainerEdits
121+
122+
for _, d := range spec.Devices {
123+
// The v0.5.0 spec allowed device names to start with a digit instead of requiring a letter
124+
if len(d.Name) > 0 && !isLetter(rune(d.Name[0])) {
125+
return true
126+
}
127+
edits = append(edits, &d.ContainerEdits)
128+
}
129+
130+
edits = append(edits, &spec.ContainerEdits)
131+
for _, e := range edits {
132+
for _, dn := range e.DeviceNodes {
133+
// The HostPath field was added in v0.5.0
134+
if dn.HostPath != "" {
135+
return true
136+
}
137+
}
138+
}
139+
return false
140+
}
141+
142+
// requiresV040 returns true if the spec uses v0.4.0 features
143+
func requiresV040(spec *cdi.Spec) bool {
144+
var edits []*cdi.ContainerEdits
145+
146+
for _, d := range spec.Devices {
147+
edits = append(edits, &d.ContainerEdits)
148+
}
149+
150+
edits = append(edits, &spec.ContainerEdits)
151+
for _, e := range edits {
152+
for _, m := range e.Mounts {
153+
// The Type field was added in v0.4.0
154+
if m.Type != "" {
155+
return true
156+
}
157+
}
158+
}
159+
return false
160+
}

0 commit comments

Comments
 (0)