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 <stephen.day@docker.com>
This commit is contained in:
parent
17901fafa0
commit
fb0688362c
77
platforms/database.go
Normal file
77
platforms/database.go
Normal file
@ -0,0 +1,77 @@
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// These function are generated from from https://golang.org/src/go/build/syslist.go.
|
||||
//
|
||||
// We use switch statements because they are slightly faster than map lookups
|
||||
// and use a little less memory.
|
||||
|
||||
// isKnownOS returns true if we know about the operating system.
|
||||
//
|
||||
// The OS value should be normalized before calling this function.
|
||||
func isKnownOS(os string) bool {
|
||||
switch os {
|
||||
case "android", "darwin", "dragonfly", "freebsd", "linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isKnownArch returns true if we know about the architecture.
|
||||
//
|
||||
// The arch value should be normalized before being passed to this function.
|
||||
func isKnownArch(arch string) bool {
|
||||
switch arch {
|
||||
case "386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le", "ppc", "s390", "s390x", "sparc", "sparc64":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeOS(os string) string {
|
||||
if os == "" {
|
||||
return runtime.GOOS
|
||||
}
|
||||
os = strings.ToLower(os)
|
||||
|
||||
switch os {
|
||||
case "macos":
|
||||
os = "darwin"
|
||||
}
|
||||
return os
|
||||
}
|
||||
|
||||
// normalizeArch normalizes the architecture.
|
||||
func normalizeArch(arch, variant string) (string, string) {
|
||||
arch, variant = strings.ToLower(arch), strings.ToLower(variant)
|
||||
switch arch {
|
||||
case "i386":
|
||||
arch = "386"
|
||||
variant = ""
|
||||
case "x86_64", "x86-64":
|
||||
arch = "amd64"
|
||||
variant = ""
|
||||
case "aarch64":
|
||||
arch = "arm64"
|
||||
variant = "" // v8 is implied
|
||||
case "armhf":
|
||||
arch = "arm"
|
||||
variant = ""
|
||||
case "armel":
|
||||
arch = "arm"
|
||||
variant = "v6"
|
||||
case "arm":
|
||||
switch variant {
|
||||
case "v7", "7":
|
||||
variant = "v7"
|
||||
case "5", "6", "8":
|
||||
variant = "v" + variant
|
||||
}
|
||||
}
|
||||
|
||||
return arch, variant
|
||||
}
|
16
platforms/defaults.go
Normal file
16
platforms/defaults.go
Normal file
@ -0,0 +1,16 @@
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Default returns the current platform's default platform specification.
|
||||
func Default() specs.Platform {
|
||||
return specs.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
// TODO(stevvooe): Need to resolve GOARM for arm hosts.
|
||||
}
|
||||
}
|
20
platforms/defaults_test.go
Normal file
20
platforms/defaults_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
expected := specs.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
p := Default()
|
||||
if !reflect.DeepEqual(p, expected) {
|
||||
t.Fatalf("default platform not as expected: %#v != %#v", p, expected)
|
||||
}
|
||||
}
|
129
platforms/platforms.go
Normal file
129
platforms/platforms.go
Normal file
@ -0,0 +1,129 @@
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type platformKey struct {
|
||||
os string
|
||||
arch string
|
||||
variant string
|
||||
}
|
||||
|
||||
var (
|
||||
selectorRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||
)
|
||||
|
||||
// ParseSelector parses the platform selector syntax into a platform
|
||||
// declaration.
|
||||
//
|
||||
// Platform selectors are in the format <os|arch>[/<arch>[/<variant>]]. The
|
||||
// minimum required information for a platform selector is the operating system
|
||||
// or architecture. If there is only a single string (no slashes), the value
|
||||
// will be matched against the known set of operating systems, then fall
|
||||
// back to the known set of architectures. The missing component will be
|
||||
// inferred based on the local environment.
|
||||
func Parse(selector string) (specs.Platform, error) {
|
||||
if strings.Contains(selector, "*") {
|
||||
// TODO(stevvooe): need to work out exact wildcard handling
|
||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: wildcards not yet supported", selector)
|
||||
}
|
||||
|
||||
parts := strings.Split(selector, "/")
|
||||
|
||||
for _, part := range parts {
|
||||
if !selectorRe.MatchString(part) {
|
||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q is an invalid component of %q: platform selector component must match %q", part, selector, selectorRe.String())
|
||||
}
|
||||
}
|
||||
|
||||
var p specs.Platform
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
// in this case, we will test that the value might be an OS, then look
|
||||
// it up. If it is not known, we'll treat it as an architecture. Since
|
||||
// we have very little information about the platform here, we are
|
||||
// going to be a little more strict if we don't know about the argument
|
||||
// value.
|
||||
p.OS = normalizeOS(parts[0])
|
||||
if isKnownOS(p.OS) {
|
||||
// picks a default architecture
|
||||
p.Architecture = runtime.GOARCH
|
||||
if p.Architecture == "arm" {
|
||||
// TODO(stevvooe): Resolve arm variant, if not v6 (default)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
p.Architecture, p.Variant = normalizeArch(parts[0], "")
|
||||
if isKnownArch(p.Architecture) {
|
||||
p.OS = runtime.GOOS
|
||||
return p, nil
|
||||
}
|
||||
|
||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: unknown operating system or architecture", selector)
|
||||
case 2:
|
||||
// In this case, we treat as a regular os/arch pair. We don't care
|
||||
// about whether or not we know of the platform.
|
||||
p.OS = normalizeOS(parts[0])
|
||||
p.Architecture, p.Variant = normalizeArch(parts[1], "")
|
||||
|
||||
return p, nil
|
||||
case 3:
|
||||
// we have a fully specified variant, this is rare
|
||||
p.OS = normalizeOS(parts[0])
|
||||
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: cannot parse platform selector", selector)
|
||||
}
|
||||
|
||||
func Match(selector string, platform specs.Platform) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Format returns a string that provides a shortened overview of the platform.
|
||||
func Format(platform specs.Platform) string {
|
||||
if platform.OS == "" {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
return joinNotEmpty(platform.OS, platform.Architecture, platform.Variant)
|
||||
}
|
||||
|
||||
func joinNotEmpty(s ...string) string {
|
||||
var ss []string
|
||||
for _, s := range s {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ss = append(ss, s)
|
||||
}
|
||||
|
||||
return strings.Join(ss, "/")
|
||||
}
|
||||
|
||||
// Normalize validates and translate the platform to the canonical value.
|
||||
//
|
||||
// For example, if "Aarch64" is encountered, we change it to "arm64" or if
|
||||
// "x86_64" is encountered, it becomes "amd64".
|
||||
func Normalize(platform specs.Platform) specs.Platform {
|
||||
platform.OS = normalizeOS(platform.OS)
|
||||
platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant)
|
||||
|
||||
// these fields are deprecated, remove them
|
||||
platform.OSFeatures = nil
|
||||
platform.OSVersion = ""
|
||||
|
||||
return platform
|
||||
}
|
233
platforms/platforms_test.go
Normal file
233
platforms/platforms_test.go
Normal file
@ -0,0 +1,233 @@
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func TestParseSelector(t *testing.T) {
|
||||
var (
|
||||
defaultOS = runtime.GOOS
|
||||
defaultArch = runtime.GOARCH
|
||||
)
|
||||
|
||||
for _, testcase := range []struct {
|
||||
skip bool
|
||||
input string
|
||||
expected specs.Platform
|
||||
formatted string
|
||||
}{
|
||||
// While wildcards are a valid use case for platform selection,
|
||||
// addressing these cases is outside the initial scope for this
|
||||
// package. When we do add platform wildcards, we should add in these
|
||||
// testcases to ensure that they are correctly represented.
|
||||
{
|
||||
skip: true,
|
||||
input: "*",
|
||||
expected: specs.Platform{
|
||||
OS: "*",
|
||||
Architecture: "*",
|
||||
},
|
||||
formatted: "*/*",
|
||||
},
|
||||
{
|
||||
skip: true,
|
||||
input: "linux/*",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "*",
|
||||
},
|
||||
formatted: "linux/*",
|
||||
},
|
||||
{
|
||||
skip: true,
|
||||
input: "*/arm64",
|
||||
expected: specs.Platform{
|
||||
OS: "*",
|
||||
Architecture: "arm64",
|
||||
},
|
||||
formatted: "*/arm64",
|
||||
},
|
||||
{
|
||||
// NOTE(stevvooe): In this case, the consumer can assume this is v7
|
||||
// but we leave the variant blank. This will represent the vast
|
||||
// majority of arm images.
|
||||
input: "linux/arm",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm",
|
||||
},
|
||||
formatted: "linux/arm",
|
||||
},
|
||||
{
|
||||
input: "linux/arm/v6",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm",
|
||||
Variant: "v6",
|
||||
},
|
||||
formatted: "linux/arm/v6",
|
||||
},
|
||||
{
|
||||
input: "linux/arm/v7",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "arm",
|
||||
Variant: "v7",
|
||||
},
|
||||
formatted: "linux/arm/v7",
|
||||
},
|
||||
{
|
||||
input: "arm",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "arm",
|
||||
},
|
||||
formatted: "linux/arm",
|
||||
},
|
||||
{
|
||||
input: "armel",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "arm",
|
||||
Variant: "v6",
|
||||
},
|
||||
formatted: "linux/arm/v6",
|
||||
},
|
||||
{
|
||||
input: "armhf",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "arm",
|
||||
},
|
||||
formatted: "linux/arm",
|
||||
},
|
||||
{
|
||||
input: "Aarch64",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "arm64",
|
||||
},
|
||||
formatted: joinNotEmpty(defaultOS, "arm64"),
|
||||
},
|
||||
{
|
||||
input: "x86_64",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "amd64",
|
||||
},
|
||||
formatted: joinNotEmpty(defaultOS, "amd64"),
|
||||
},
|
||||
{
|
||||
input: "Linux/x86_64",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
formatted: "linux/amd64",
|
||||
},
|
||||
{
|
||||
input: "i386",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "386",
|
||||
},
|
||||
formatted: joinNotEmpty(defaultOS, "386"),
|
||||
},
|
||||
{
|
||||
input: "linux",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: defaultArch,
|
||||
},
|
||||
formatted: joinNotEmpty("linux", defaultArch),
|
||||
},
|
||||
{
|
||||
input: "s390x",
|
||||
expected: specs.Platform{
|
||||
OS: defaultOS,
|
||||
Architecture: "s390x",
|
||||
},
|
||||
formatted: joinNotEmpty(defaultOS, "s390x"),
|
||||
},
|
||||
{
|
||||
input: "linux/s390x",
|
||||
expected: specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "s390x",
|
||||
},
|
||||
formatted: "linux/s390x",
|
||||
},
|
||||
{
|
||||
input: "macOS",
|
||||
expected: specs.Platform{
|
||||
OS: "darwin",
|
||||
Architecture: defaultArch,
|
||||
},
|
||||
formatted: joinNotEmpty("darwin", defaultArch),
|
||||
},
|
||||
} {
|
||||
t.Run(testcase.input, func(t *testing.T) {
|
||||
if testcase.skip {
|
||||
t.Skip("this case is not yet supported")
|
||||
}
|
||||
p, err := Parse(testcase.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(p, testcase.expected) {
|
||||
t.Fatalf("platform did not match expected: %#v != %#v", p, testcase.expected)
|
||||
}
|
||||
|
||||
formatted := Format(p)
|
||||
if formatted != testcase.formatted {
|
||||
t.Fatalf("unexpected format: %q != %q", formatted, testcase.formatted)
|
||||
}
|
||||
|
||||
// re-parse the formatted output and ensure we are stable
|
||||
reparsed, err := Parse(formatted)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing formatted output: %v", err)
|
||||
}
|
||||
|
||||
if Format(reparsed) != formatted {
|
||||
t.Fatalf("normalized output did not survive the round trip: %v != %v", Format(reparsed), formatted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSelectorInvalid(t *testing.T) {
|
||||
for _, testcase := range []struct {
|
||||
input string
|
||||
}{
|
||||
{
|
||||
input: "", // empty
|
||||
},
|
||||
{
|
||||
input: "/linux/arm", // leading slash
|
||||
},
|
||||
{
|
||||
input: "linux/arm/", // trailing slash
|
||||
},
|
||||
{
|
||||
input: "linux /arm", // spaces
|
||||
},
|
||||
{
|
||||
input: "linux/&arm", // invalid character
|
||||
},
|
||||
{
|
||||
input: "linux/arm/foo/bar", // too mayn components
|
||||
},
|
||||
} {
|
||||
t.Run(testcase.input, func(t *testing.T) {
|
||||
if _, err := Parse(testcase.input); err == nil {
|
||||
t.Fatalf("should have received an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user