Skip to content

Commit b42036f

Browse files
authored
Merge pull request #16 from dmcgowan/add-os-features
Add support for OS Features in the format
2 parents 005d370 + 2474351 commit b42036f

4 files changed

Lines changed: 80 additions & 17 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/containerd/platforms
22

3-
go 1.20
3+
go 1.21
44

55
require (
66
github.com/containerd/log v0.1.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2121
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2222
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2323
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
24+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

platforms.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,18 @@ import (
114114
"path"
115115
"regexp"
116116
"runtime"
117+
"slices"
117118
"strconv"
118119
"strings"
119120

120121
specs "github.com/opencontainers/image-spec/specs-go/v1"
121122
)
122123

123124
var (
124-
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
125-
osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`)
125+
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)
126+
osRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)((?:\+[A-Za-z0-9_.-]+)*)\))?$`)
126127
)
127128

128-
const osAndVersionFormat = "%s(%s)"
129-
130129
// Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
131130
type Platform = specs.Platform
132131

@@ -210,11 +209,14 @@ func ParseAll(specifiers []string) ([]specs.Platform, error) {
210209

211210
// Parse parses the platform specifier syntax into a platform declaration.
212211
//
213-
// Platform specifiers are in the format `<os>[(<OSVersion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]`.
212+
// Platform specifiers are in the format `<os>[(<os options>)]|<arch>|<os>[(<os options>)]/<arch>[/<variant>]`.
214213
// The minimum required information for a platform specifier is the operating
215-
// system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
216-
// When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
217-
// and an empty string otherwise.
214+
// system or architecture. The "os options" may be OSVersion which can be part of the OS
215+
// like `windows(10.0.17763)`. When an OSVersion is specified, then specs.Platform.OSVersion is
216+
// populated with that value, and an empty string otherwise. The "os options" may also include an
217+
// array of OSFeatures, each feature prefixed with '+', without any other separator, and provided
218+
// after the OSVersion when the OSVersion is specified. An "os options" with version and features
219+
// is like `windows(10.0.17763+win32k)`.
218220
// If there is only a single string (no slashes), the
219221
// value will be matched against the known set of operating systems, then fall
220222
// back to the known set of architectures. The missing component will be
@@ -231,14 +233,17 @@ func Parse(specifier string) (specs.Platform, error) {
231233
var p specs.Platform
232234
for i, part := range parts {
233235
if i == 0 {
234-
// First element is <os>[(<OSVersion>)]
235-
osVer := osAndVersionRe.FindStringSubmatch(part)
236-
if osVer == nil {
237-
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osAndVersionRe.String(), errInvalidArgument)
236+
// First element is <os>[(<OSVersion>[+<OSFeature>]*)]
237+
osOptions := osRe.FindStringSubmatch(part)
238+
if osOptions == nil {
239+
return specs.Platform{}, fmt.Errorf("%q is an invalid OS component of %q: OSAndVersion specifier component must match %q: %w", part, specifier, osRe.String(), errInvalidArgument)
238240
}
239241

240-
p.OS = normalizeOS(osVer[1])
241-
p.OSVersion = osVer[2]
242+
p.OS = normalizeOS(osOptions[1])
243+
p.OSVersion = osOptions[2]
244+
if osOptions[3] != "" {
245+
p.OSFeatures = strings.Split(osOptions[3][1:], "+")
246+
}
242247
} else {
243248
if !specifierRe.MatchString(part) {
244249
return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
@@ -322,8 +327,17 @@ func FormatAll(platform specs.Platform) string {
322327
return "unknown"
323328
}
324329

325-
if platform.OSVersion != "" {
326-
OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
330+
osOptions := platform.OSVersion
331+
features := platform.OSFeatures
332+
if !slices.IsSorted(features) {
333+
features = slices.Clone(features)
334+
slices.Sort(features)
335+
}
336+
if len(features) > 0 {
337+
osOptions += "+" + strings.Join(features, "+")
338+
}
339+
if osOptions != "" {
340+
OSAndVersion := fmt.Sprintf("%s(%s)", platform.OS, osOptions)
327341
return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
328342
}
329343
return path.Join(platform.OS, platform.Architecture, platform.Variant)

platforms_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,54 @@ func TestParseSelector(t *testing.T) {
343343
formatted: path.Join("windows(10.0.17763)", defaultArch, defaultVariant),
344344
useV2Format: true,
345345
},
346+
{
347+
input: "windows(10.0.17763+win32k)",
348+
expected: specs.Platform{
349+
OS: "windows",
350+
OSVersion: "10.0.17763",
351+
OSFeatures: []string{"win32k"},
352+
Architecture: defaultArch,
353+
Variant: defaultVariant,
354+
},
355+
formatted: path.Join("windows(10.0.17763+win32k)", defaultArch, defaultVariant),
356+
useV2Format: true,
357+
},
358+
{
359+
input: "linux(+gpu)",
360+
expected: specs.Platform{
361+
OS: "linux",
362+
OSVersion: "",
363+
OSFeatures: []string{"gpu"},
364+
Architecture: defaultArch,
365+
Variant: defaultVariant,
366+
},
367+
formatted: path.Join("linux(+gpu)", defaultArch, defaultVariant),
368+
useV2Format: true,
369+
},
370+
{
371+
input: "linux(+gpu+simd)",
372+
expected: specs.Platform{
373+
OS: "linux",
374+
OSVersion: "",
375+
OSFeatures: []string{"gpu", "simd"},
376+
Architecture: defaultArch,
377+
Variant: defaultVariant,
378+
},
379+
formatted: path.Join("linux(+gpu+simd)", defaultArch, defaultVariant),
380+
useV2Format: true,
381+
},
382+
{
383+
input: "linux(+unsorted+erofs)",
384+
expected: specs.Platform{
385+
OS: "linux",
386+
OSVersion: "",
387+
OSFeatures: []string{"unsorted", "erofs"},
388+
Architecture: defaultArch,
389+
Variant: defaultVariant,
390+
},
391+
formatted: path.Join("linux(+erofs+unsorted)", defaultArch, defaultVariant),
392+
useV2Format: true,
393+
},
346394
} {
347395
t.Run(testcase.input, func(t *testing.T) {
348396
if testcase.skip {

0 commit comments

Comments
 (0)