Skip to content

Commit fb06883

Browse files
committed
platforms: define selectors for platforms
For supporting selection of images and runtimes in the containerized world, there is thin support for selecting objects by platform. This package defines a syntax to display to users that can express specific platforms in addition to wild cards for matching platforms. The plan is to extend this to provide support for parsing flag arguments and displaying platform types for images. This package will also provide a configurable matcher to allow match of platforms against arbitrary targets, invariant to the Go compilation. The internals are based the OCI Image Spec model. This changeset is being submitted for feedback on the approach before this gets larger. Specifically, review the unit tests and raise any concerns. Signed-off-by: Stephen J Day <[email protected]>
1 parent 17901fa commit fb06883

5 files changed

Lines changed: 475 additions & 0 deletions

File tree

platforms/database.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package platforms
2+
3+
import (
4+
"runtime"
5+
"strings"
6+
)
7+
8+
// These function are generated from from https://golang.org/src/go/build/syslist.go.
9+
//
10+
// We use switch statements because they are slightly faster than map lookups
11+
// and use a little less memory.
12+
13+
// isKnownOS returns true if we know about the operating system.
14+
//
15+
// The OS value should be normalized before calling this function.
16+
func isKnownOS(os string) bool {
17+
switch os {
18+
case "android", "darwin", "dragonfly", "freebsd", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos":
19+
return true
20+
}
21+
return false
22+
}
23+
24+
// isKnownArch returns true if we know about the architecture.
25+
//
26+
// The arch value should be normalized before being passed to this function.
27+
func isKnownArch(arch string) bool {
28+
switch arch {
29+
case "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le", "ppc", "s390", "s390x", "sparc", "sparc64":
30+
return true
31+
}
32+
return false
33+
}
34+
35+
func normalizeOS(os string) string {
36+
if os == "" {
37+
return runtime.GOOS
38+
}
39+
os = strings.ToLower(os)
40+
41+
switch os {
42+
case "macos":
43+
os = "darwin"
44+
}
45+
return os
46+
}
47+
48+
// normalizeArch normalizes the architecture.
49+
func normalizeArch(arch, variant string) (string, string) {
50+
arch, variant = strings.ToLower(arch), strings.ToLower(variant)
51+
switch arch {
52+
case "i386":
53+
arch = "386"
54+
variant = ""
55+
case "x86_64", "x86-64":
56+
arch = "amd64"
57+
variant = ""
58+
case "aarch64":
59+
arch = "arm64"
60+
variant = "" // v8 is implied
61+
case "armhf":
62+
arch = "arm"
63+
variant = ""
64+
case "armel":
65+
arch = "arm"
66+
variant = "v6"
67+
case "arm":
68+
switch variant {
69+
case "v7", "7":
70+
variant = "v7"
71+
case "5", "6", "8":
72+
variant = "v" + variant
73+
}
74+
}
75+
76+
return arch, variant
77+
}

platforms/defaults.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package platforms
2+
3+
import (
4+
"runtime"
5+
6+
specs "github.com/opencontainers/image-spec/specs-go/v1"
7+
)
8+
9+
// Default returns the current platform's default platform specification.
10+
func Default() specs.Platform {
11+
return specs.Platform{
12+
OS: runtime.GOOS,
13+
Architecture: runtime.GOARCH,
14+
// TODO(stevvooe): Need to resolve GOARM for arm hosts.
15+
}
16+
}

platforms/defaults_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package platforms
2+
3+
import (
4+
"reflect"
5+
"runtime"
6+
"testing"
7+
8+
specs "github.com/opencontainers/image-spec/specs-go/v1"
9+
)
10+
11+
func TestDefault(t *testing.T) {
12+
expected := specs.Platform{
13+
OS: runtime.GOOS,
14+
Architecture: runtime.GOARCH,
15+
}
16+
p := Default()
17+
if !reflect.DeepEqual(p, expected) {
18+
t.Fatalf("default platform not as expected: %#v != %#v", p, expected)
19+
}
20+
}

platforms/platforms.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package platforms
2+
3+
import (
4+
"regexp"
5+
"runtime"
6+
"strings"
7+
8+
"github.com/containerd/containerd/errdefs"
9+
specs "github.com/opencontainers/image-spec/specs-go/v1"
10+
"github.com/pkg/errors"
11+
)
12+
13+
type platformKey struct {
14+
os string
15+
arch string
16+
variant string
17+
}
18+
19+
var (
20+
selectorRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
21+
)
22+
23+
// ParseSelector parses the platform selector syntax into a platform
24+
// declaration.
25+
//
26+
// Platform selectors are in the format <os|arch>[/<arch>[/<variant>]]. The
27+
// minimum required information for a platform selector is the operating system
28+
// or architecture. If there is only a single string (no slashes), the value
29+
// will be matched against the known set of operating systems, then fall
30+
// back to the known set of architectures. The missing component will be
31+
// inferred based on the local environment.
32+
func Parse(selector string) (specs.Platform, error) {
33+
if strings.Contains(selector, "*") {
34+
// TODO(stevvooe): need to work out exact wildcard handling
35+
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: wildcards not yet supported", selector)
36+
}
37+
38+
parts := strings.Split(selector, "/")
39+
40+
for _, part := range parts {
41+
if !selectorRe.MatchString(part) {
42+
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q is an invalid component of %q: platform selector component must match %q", part, selector, selectorRe.String())
43+
}
44+
}
45+
46+
var p specs.Platform
47+
switch len(parts) {
48+
case 1:
49+
// in this case, we will test that the value might be an OS, then look
50+
// it up. If it is not known, we'll treat it as an architecture. Since
51+
// we have very little information about the platform here, we are
52+
// going to be a little more strict if we don't know about the argument
53+
// value.
54+
p.OS = normalizeOS(parts[0])
55+
if isKnownOS(p.OS) {
56+
// picks a default architecture
57+
p.Architecture = runtime.GOARCH
58+
if p.Architecture == "arm" {
59+
// TODO(stevvooe): Resolve arm variant, if not v6 (default)
60+
}
61+
62+
return p, nil
63+
}
64+
65+
p.Architecture, p.Variant = normalizeArch(parts[0], "")
66+
if isKnownArch(p.Architecture) {
67+
p.OS = runtime.GOOS
68+
return p, nil
69+
}
70+
71+
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: unknown operating system or architecture", selector)
72+
case 2:
73+
// In this case, we treat as a regular os/arch pair. We don't care
74+
// about whether or not we know of the platform.
75+
p.OS = normalizeOS(parts[0])
76+
p.Architecture, p.Variant = normalizeArch(parts[1], "")
77+
78+
return p, nil
79+
case 3:
80+
// we have a fully specified variant, this is rare
81+
p.OS = normalizeOS(parts[0])
82+
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
83+
84+
return p, nil
85+
}
86+
87+
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: cannot parse platform selector", selector)
88+
}
89+
90+
func Match(selector string, platform specs.Platform) bool {
91+
return false
92+
}
93+
94+
// Format returns a string that provides a shortened overview of the platform.
95+
func Format(platform specs.Platform) string {
96+
if platform.OS == "" {
97+
return "unknown"
98+
}
99+
100+
return joinNotEmpty(platform.OS, platform.Architecture, platform.Variant)
101+
}
102+
103+
func joinNotEmpty(s ...string) string {
104+
var ss []string
105+
for _, s := range s {
106+
if s == "" {
107+
continue
108+
}
109+
110+
ss = append(ss, s)
111+
}
112+
113+
return strings.Join(ss, "/")
114+
}
115+
116+
// Normalize validates and translate the platform to the canonical value.
117+
//
118+
// For example, if "Aarch64" is encountered, we change it to "arm64" or if
119+
// "x86_64" is encountered, it becomes "amd64".
120+
func Normalize(platform specs.Platform) specs.Platform {
121+
platform.OS = normalizeOS(platform.OS)
122+
platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant)
123+
124+
// these fields are deprecated, remove them
125+
platform.OSFeatures = nil
126+
platform.OSVersion = ""
127+
128+
return platform
129+
}

0 commit comments

Comments
 (0)