Skip to content

Commit 893254c

Browse files
committed
Add MinimumRequiredVersion function to spec
This change adds a function to get the minimum required version for a spec. This checks the fields of the spec and retuns the latest version for non-empty fields. For example, the `Mount.Type` field was added in v0.4.0 and the `DeviceNode.HostPath` field was added in v0.5.0. Signed-off-by: Evan Lezar <[email protected]>
1 parent e93a674 commit 893254c

File tree

4 files changed

+259
-4
lines changed

4 files changed

+259
-4
lines changed

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/spec.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ 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
)
@@ -250,6 +247,12 @@ func validateVersion(version string) error {
250247
return nil
251248
}
252249

250+
// MinimumRequiredVersion checks the minimum required version for the spec
251+
func (s *Spec) MinimumRequiredVersion() (string, error) {
252+
minVersion := required.minVersion(s.Spec)
253+
return minVersion.String(), nil
254+
}
255+
253256
// ParseSpec parses CDI Spec data into a raw CDI Spec.
254257
func ParseSpec(data []byte) (*cdi.Spec, error) {
255258
var raw *cdi.Spec

pkg/cdi/spec_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,119 @@ 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.2.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.2.0",
632+
},
633+
}
634+
635+
for _, tc := range testCases {
636+
t.Run(tc.description, func(t *testing.T) {
637+
s := Spec{Spec: tc.spec}
638+
639+
v, err := s.MinimumRequiredVersion()
640+
require.NoError(t, err)
641+
642+
require.Equal(t, tc.expectedVersion, v)
643+
})
644+
}
645+
}

pkg/cdi/version.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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 vesion of the CDI Spec.
29+
CurrentVersion = cdi.CurrentVersion
30+
31+
vCurrent version = "v" + CurrentVersion
32+
33+
vEarliest version = v020
34+
35+
v020 version = "v0.2.0"
36+
v030 version = "v0.3.0"
37+
v040 version = "v0.4.0"
38+
v050 version = "v0.5.0"
39+
)
40+
41+
// version represents a semantic version string
42+
type version string
43+
44+
// String returns the string representation of the version.
45+
// This trims a leading v if present.
46+
func (v version) String() string {
47+
return strings.TrimPrefix(string(v), "v")
48+
}
49+
50+
// LT checks whether a version is less than the specified version.
51+
// Semantic versioning is used to perform the comparison.
52+
func (v version) LT(o version) bool {
53+
return semver.Compare(string(v), string(o)) < 0
54+
}
55+
56+
// IsLatest checks whether the version is the latest supported version
57+
func (v version) IsLatest() bool {
58+
return v == vCurrent
59+
}
60+
61+
type requiredFunc func(*cdi.Spec) bool
62+
63+
type requiredVersionMap map[version]requiredFunc
64+
65+
// required stores a map of spec versions to functions to check the required versions.
66+
// Adding new fields / spec versions requires that a `requiredFunc` be implemented and
67+
// this map be updated.
68+
var required = requiredVersionMap{
69+
v050: requiresV050,
70+
v040: requiresV040,
71+
}
72+
73+
// minVersion returns the minimum version required for the given spec
74+
func (r requiredVersionMap) minVersion(spec *cdi.Spec) version {
75+
minVersion := vEarliest
76+
77+
for specVersion := range validSpecVersions {
78+
v := version("v" + strings.TrimPrefix(specVersion, "v"))
79+
if f, ok := r[v]; ok {
80+
if f(spec) && minVersion.LT(v) {
81+
minVersion = v
82+
}
83+
}
84+
// If we have already detected the latest version then no later version could be detected
85+
if minVersion.IsLatest() {
86+
break
87+
}
88+
}
89+
90+
return minVersion
91+
}
92+
93+
// requiresV050 returns true if the spec uses v0.5.0 features
94+
func requiresV050(spec *cdi.Spec) bool {
95+
var edits []*cdi.ContainerEdits
96+
97+
for _, d := range spec.Devices {
98+
// The v0.5.0 spec allowed device names to start with a digit instead of requiring a letter
99+
if len(d.Name) > 0 && !isLetter(rune(d.Name[0])) {
100+
return true
101+
}
102+
edits = append(edits, &d.ContainerEdits)
103+
}
104+
105+
edits = append(edits, &spec.ContainerEdits)
106+
for _, e := range edits {
107+
for _, dn := range e.DeviceNodes {
108+
// The HostPath field was added in v0.5.0
109+
if dn.HostPath != "" {
110+
return true
111+
}
112+
}
113+
}
114+
return false
115+
}
116+
117+
// requiresV040 returns true if the spec uses v0.4.0 features
118+
func requiresV040(spec *cdi.Spec) bool {
119+
var edits []*cdi.ContainerEdits
120+
121+
for _, d := range spec.Devices {
122+
edits = append(edits, &d.ContainerEdits)
123+
}
124+
125+
edits = append(edits, &spec.ContainerEdits)
126+
for _, e := range edits {
127+
for _, m := range e.Mounts {
128+
// The Type field was added in v0.4.0
129+
if m.Type != "" {
130+
return true
131+
}
132+
}
133+
}
134+
return false
135+
}

0 commit comments

Comments
 (0)