platforms: implement matcher support
Matching support is now implemented in the platforms package. The `Parse` function now returns a matcher object that can be used to match OCI platform specifications. We define this as an interface to allow the creation of helpers oriented around platform selection. Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
parent
fb0688362c
commit
94f6be5f10
@ -66,7 +66,7 @@ func (s *walkingDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts
|
|||||||
if strings.HasSuffix(desc.MediaType, ".tar.gzip") || strings.HasSuffix(desc.MediaType, ".tar+gzip") {
|
if strings.HasSuffix(desc.MediaType, ".tar.gzip") || strings.HasSuffix(desc.MediaType, ".tar+gzip") {
|
||||||
isCompressed = true
|
isCompressed = true
|
||||||
} else if !strings.HasSuffix(desc.MediaType, ".tar") {
|
} else if !strings.HasSuffix(desc.MediaType, ".tar") {
|
||||||
return emptyDesc, errors.Wrapf(errdefs.ErrNotSupported, "unsupported diff media type: %v", desc.MediaType)
|
return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", desc.MediaType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ func (s *walkingDiff) DiffMounts(ctx context.Context, lower, upper []mount.Mount
|
|||||||
media = ocispec.MediaTypeImageLayerGzip
|
media = ocispec.MediaTypeImageLayerGzip
|
||||||
isCompressed = true
|
isCompressed = true
|
||||||
default:
|
default:
|
||||||
return emptyDesc, errors.Wrapf(errdefs.ErrNotSupported, "unsupported diff media type: %v", media)
|
return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", media)
|
||||||
}
|
}
|
||||||
aDir, err := ioutil.TempDir("", "left-")
|
aDir, err := ioutil.TempDir("", "left-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -26,7 +26,7 @@ var (
|
|||||||
ErrAlreadyExists = errors.New("already exists")
|
ErrAlreadyExists = errors.New("already exists")
|
||||||
ErrFailedPrecondition = errors.New("failed precondition")
|
ErrFailedPrecondition = errors.New("failed precondition")
|
||||||
ErrUnavailable = errors.New("unavailable")
|
ErrUnavailable = errors.New("unavailable")
|
||||||
ErrNotSupported = errors.New("not supported") // represents not supported and unimplemented
|
ErrNotImplemented = errors.New("not implemented") // represents not supported and unimplemented
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsInvalidArgument(err error) bool {
|
func IsInvalidArgument(err error) bool {
|
||||||
@ -54,6 +54,6 @@ func IsUnavailable(err error) bool {
|
|||||||
return errors.Cause(err) == ErrUnavailable
|
return errors.Cause(err) == ErrUnavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsNotSupported(err error) bool {
|
func IsNotImplemented(err error) bool {
|
||||||
return errors.Cause(err) == ErrNotSupported
|
return errors.Cause(err) == ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func ToGRPC(err error) error {
|
|||||||
return grpc.Errorf(codes.FailedPrecondition, err.Error())
|
return grpc.Errorf(codes.FailedPrecondition, err.Error())
|
||||||
case IsUnavailable(err):
|
case IsUnavailable(err):
|
||||||
return grpc.Errorf(codes.Unavailable, err.Error())
|
return grpc.Errorf(codes.Unavailable, err.Error())
|
||||||
case IsNotSupported(err):
|
case IsNotImplemented(err):
|
||||||
return grpc.Errorf(codes.Unimplemented, err.Error())
|
return grpc.Errorf(codes.Unimplemented, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ func FromGRPC(err error) error {
|
|||||||
case codes.FailedPrecondition:
|
case codes.FailedPrecondition:
|
||||||
cls = ErrFailedPrecondition
|
cls = ErrFailedPrecondition
|
||||||
case codes.Unimplemented:
|
case codes.Unimplemented:
|
||||||
cls = ErrNotSupported
|
cls = ErrNotImplemented
|
||||||
default:
|
default:
|
||||||
cls = ErrUnknown
|
cls = ErrUnknown
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,92 @@
|
|||||||
|
// Package platforms provides a toolkit for normalizing, matching and
|
||||||
|
// specifying container platforms.
|
||||||
|
//
|
||||||
|
// Centered around OCI platform specifications, we define a string-based
|
||||||
|
// specifier syntax that can be used for user input. With a specifier, users
|
||||||
|
// only need to specify the parts of the platform that are relevant to their
|
||||||
|
// context, providing an operating system or architecture or both.
|
||||||
|
//
|
||||||
|
// How do I use this package?
|
||||||
|
//
|
||||||
|
// The vast majority of use cases should simply use the match function with
|
||||||
|
// user input. The first step is to parse a specifier into a matcher:
|
||||||
|
//
|
||||||
|
// m, err := Parse("linux")
|
||||||
|
// if err != nil { ... }
|
||||||
|
//
|
||||||
|
// Once you have a matcher, use it to match against the platform declared by a
|
||||||
|
// component, typically from an image or runtime. Since extracting an images
|
||||||
|
// platform is a little more involved, we'll use an example against the
|
||||||
|
// platform default:
|
||||||
|
//
|
||||||
|
// if ok := m.Match(Default()); !ok { /* doesn't match */ }
|
||||||
|
//
|
||||||
|
// This can be composed in loops for resolving runtimes or used as a filter for
|
||||||
|
// fetch and select images.
|
||||||
|
//
|
||||||
|
// More details of the specifier syntax and platform spec follow.
|
||||||
|
//
|
||||||
|
// Declaring Platform Support
|
||||||
|
//
|
||||||
|
// Components that have strict platform requirements should use the OCI
|
||||||
|
// platform specification to declare their support. Typically, this will be
|
||||||
|
// images and runtimes that should make these declaring which platform they
|
||||||
|
// support specifically. This looks roughly as follows:
|
||||||
|
//
|
||||||
|
// type Platform struct {
|
||||||
|
// Architecture string
|
||||||
|
// OS string
|
||||||
|
// Variant string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Most images and runtimes should at least set Architecture and OS, according
|
||||||
|
// to their GOARCH and GOOS values, respectively (follow the OCI image
|
||||||
|
// specification when in doubt). ARM should set variant under certain
|
||||||
|
// discussions, which are outlined below.
|
||||||
|
//
|
||||||
|
// Platform Specifiers
|
||||||
|
//
|
||||||
|
// While the OCI platform specifications provide a tool for components to
|
||||||
|
// specify structured information, user input typically doesn't need the full
|
||||||
|
// context and much can be inferred. To solve this problem, we introduced
|
||||||
|
// "specifiers". A specifier has the format `<os|arch>[/<arch>[/<variant>]]`.
|
||||||
|
// The user can provide either the operating system or the architecture or both.
|
||||||
|
//
|
||||||
|
// An example of a common specifier is `linux/amd64`. If the host has a default
|
||||||
|
// of runtime that matches this, the user can simply provide the component that
|
||||||
|
// matters. For example, if a image provides amd64 and arm64 support, the
|
||||||
|
// operating system, `linux` can be inferred, so they only have to provide
|
||||||
|
// `arm64` or `amd64`. Similar behavior is implemented for operating systems,
|
||||||
|
// where the architecture may be known but a runtime may support images from
|
||||||
|
// different operating systems.
|
||||||
|
//
|
||||||
|
// Normalization
|
||||||
|
//
|
||||||
|
// Because not all users are familiar with the way the Go runtime represents
|
||||||
|
// platforms, several normalizations have been provided to make this package
|
||||||
|
// easier to user.
|
||||||
|
//
|
||||||
|
// The following are performed for architectures:
|
||||||
|
//
|
||||||
|
// Value Normalized
|
||||||
|
// aarch64 arm64
|
||||||
|
// armhf arm
|
||||||
|
// armel arm/v6
|
||||||
|
// i386 386
|
||||||
|
// x86_64 amd64
|
||||||
|
// x86-64 amd64
|
||||||
|
//
|
||||||
|
// We also normalize the operating system `macos` to `darwin`.
|
||||||
|
//
|
||||||
|
// ARM Support
|
||||||
|
//
|
||||||
|
// To qualify ARM architecture, the Variant field is used to qualify the arm
|
||||||
|
// version. The most common arm version, v7, is represented without the variant
|
||||||
|
// unless it is explicitly provided. This is treated as equivalent to armhf. A
|
||||||
|
// previous architecture, armel, will be normalized to arm/v6.
|
||||||
|
//
|
||||||
|
// While these normalizations are provided, their support on arm platforms has
|
||||||
|
// not yet been fully implemented and tested.
|
||||||
package platforms
|
package platforms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,36 +99,56 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type platformKey struct {
|
|
||||||
os string
|
|
||||||
arch string
|
|
||||||
variant string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
selectorRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseSelector parses the platform selector syntax into a platform
|
// Matcher matches platforms specifications, provided by an image or runtime.
|
||||||
// declaration.
|
type Matcher interface {
|
||||||
|
Spec() specs.Platform
|
||||||
|
Match(platform specs.Platform) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type matcher struct {
|
||||||
|
specs.Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matcher) Spec() specs.Platform {
|
||||||
|
return m.Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matcher) Match(platform specs.Platform) bool {
|
||||||
|
normalized := Normalize(platform)
|
||||||
|
return m.OS == normalized.OS &&
|
||||||
|
m.Architecture == normalized.Architecture &&
|
||||||
|
m.Variant == normalized.Variant
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *matcher) String() string {
|
||||||
|
return Format(m.Platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses the platform specifier syntax into a platform declaration.
|
||||||
//
|
//
|
||||||
// Platform selectors are in the format <os|arch>[/<arch>[/<variant>]]. The
|
// Platform specifiers are in the format <os|arch>[/<arch>[/<variant>]]. The
|
||||||
// minimum required information for a platform selector is the operating system
|
// minimum required information for a platform specifier is the operating system
|
||||||
// or architecture. If there is only a single string (no slashes), the value
|
// 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
|
// will be matched against the known set of operating systems, then fall
|
||||||
// back to the known set of architectures. The missing component will be
|
// back to the known set of architectures. The missing component will be
|
||||||
// inferred based on the local environment.
|
// inferred based on the local environment.
|
||||||
func Parse(selector string) (specs.Platform, error) {
|
//
|
||||||
if strings.Contains(selector, "*") {
|
// Applications should opt to use `Match` over directly parsing specifiers.
|
||||||
|
func Parse(specifier string) (Matcher, error) {
|
||||||
|
if strings.Contains(specifier, "*") {
|
||||||
// TODO(stevvooe): need to work out exact wildcard handling
|
// TODO(stevvooe): need to work out exact wildcard handling
|
||||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: wildcards not yet supported", selector)
|
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: wildcards not yet supported", specifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(selector, "/")
|
parts := strings.Split(specifier, "/")
|
||||||
|
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if !selectorRe.MatchString(part) {
|
if !specifierRe.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())
|
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%q is an invalid component of %q: platform specifier component must match %q", part, specifier, specifierRe.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,41 +166,38 @@ func Parse(selector string) (specs.Platform, error) {
|
|||||||
p.Architecture = runtime.GOARCH
|
p.Architecture = runtime.GOARCH
|
||||||
if p.Architecture == "arm" {
|
if p.Architecture == "arm" {
|
||||||
// TODO(stevvooe): Resolve arm variant, if not v6 (default)
|
// TODO(stevvooe): Resolve arm variant, if not v6 (default)
|
||||||
|
return nil, errors.Wrapf(errdefs.ErrNotImplemented, "arm support not fully implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return &matcher{p}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Architecture, p.Variant = normalizeArch(parts[0], "")
|
p.Architecture, p.Variant = normalizeArch(parts[0], "")
|
||||||
if isKnownArch(p.Architecture) {
|
if isKnownArch(p.Architecture) {
|
||||||
p.OS = runtime.GOOS
|
p.OS = runtime.GOOS
|
||||||
return p, nil
|
return &matcher{p}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: unknown operating system or architecture", selector)
|
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: unknown operating system or architecture", specifier)
|
||||||
case 2:
|
case 2:
|
||||||
// In this case, we treat as a regular os/arch pair. We don't care
|
// In this case, we treat as a regular os/arch pair. We don't care
|
||||||
// about whether or not we know of the platform.
|
// about whether or not we know of the platform.
|
||||||
p.OS = normalizeOS(parts[0])
|
p.OS = normalizeOS(parts[0])
|
||||||
p.Architecture, p.Variant = normalizeArch(parts[1], "")
|
p.Architecture, p.Variant = normalizeArch(parts[1], "")
|
||||||
|
|
||||||
return p, nil
|
return &matcher{p}, nil
|
||||||
case 3:
|
case 3:
|
||||||
// we have a fully specified variant, this is rare
|
// we have a fully specified variant, this is rare
|
||||||
p.OS = normalizeOS(parts[0])
|
p.OS = normalizeOS(parts[0])
|
||||||
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
|
p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
|
||||||
|
|
||||||
return p, nil
|
return &matcher{p}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: cannot parse platform selector", selector)
|
return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: cannot parse platform specifier", specifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Match(selector string, platform specs.Platform) bool {
|
// Format returns a string specifier from the provided platform specification.
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format returns a string that provides a shortened overview of the platform.
|
|
||||||
func Format(platform specs.Platform) string {
|
func Format(platform specs.Platform) string {
|
||||||
if platform.OS == "" {
|
if platform.OS == "" {
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package platforms
|
package platforms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
@ -174,16 +175,25 @@ func TestParseSelector(t *testing.T) {
|
|||||||
if testcase.skip {
|
if testcase.skip {
|
||||||
t.Skip("this case is not yet supported")
|
t.Skip("this case is not yet supported")
|
||||||
}
|
}
|
||||||
p, err := Parse(testcase.input)
|
m, err := Parse(testcase.input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(p, testcase.expected) {
|
if !reflect.DeepEqual(m.Spec(), testcase.expected) {
|
||||||
t.Fatalf("platform did not match expected: %#v != %#v", p, testcase.expected)
|
t.Fatalf("platform did not match expected: %#v != %#v", m.Spec(), testcase.expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted := Format(p)
|
// ensure that match works on the input to the output.
|
||||||
|
if ok := m.Match(testcase.expected); !ok {
|
||||||
|
t.Fatalf("expected specifier %q matches %v", testcase.input, testcase.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fmt.Sprint(m) != testcase.formatted {
|
||||||
|
t.Fatalf("unexpected matcher string: %q != %q", fmt.Sprint(m), testcase.formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := Format(m.Spec())
|
||||||
if formatted != testcase.formatted {
|
if formatted != testcase.formatted {
|
||||||
t.Fatalf("unexpected format: %q != %q", formatted, testcase.formatted)
|
t.Fatalf("unexpected format: %q != %q", formatted, testcase.formatted)
|
||||||
}
|
}
|
||||||
@ -194,8 +204,8 @@ func TestParseSelector(t *testing.T) {
|
|||||||
t.Fatalf("error parsing formatted output: %v", err)
|
t.Fatalf("error parsing formatted output: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Format(reparsed) != formatted {
|
if Format(reparsed.Spec()) != formatted {
|
||||||
t.Fatalf("normalized output did not survive the round trip: %v != %v", Format(reparsed), formatted)
|
t.Fatalf("normalized output did not survive the round trip: %v != %v", Format(reparsed.Spec()), formatted)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -221,7 +231,7 @@ func TestParseSelectorInvalid(t *testing.T) {
|
|||||||
input: "linux/&arm", // invalid character
|
input: "linux/&arm", // invalid character
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "linux/arm/foo/bar", // too mayn components
|
input: "linux/arm/foo/bar", // too many components
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(testcase.input, func(t *testing.T) {
|
t.Run(testcase.input, func(t *testing.T) {
|
||||||
|
@ -74,7 +74,7 @@ func (s *service) Apply(ctx context.Context, er *diffapi.ApplyRequest) (*diffapi
|
|||||||
|
|
||||||
for _, differ := range s.differs {
|
for _, differ := range s.differs {
|
||||||
ocidesc, err = differ.Apply(ctx, desc, mounts)
|
ocidesc, err = differ.Apply(ctx, desc, mounts)
|
||||||
if !errdefs.IsNotSupported(err) {
|
if !errdefs.IsNotImplemented(err) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ func (s *service) Diff(ctx context.Context, dr *diffapi.DiffRequest) (*diffapi.D
|
|||||||
|
|
||||||
for _, differ := range s.differs {
|
for _, differ := range s.differs {
|
||||||
ocidesc, err = differ.DiffMounts(ctx, aMounts, bMounts, dr.MediaType, dr.Ref)
|
ocidesc, err = differ.DiffMounts(ctx, aMounts, bMounts, dr.MediaType, dr.Ref)
|
||||||
if !errdefs.IsNotSupported(err) {
|
if !errdefs.IsNotImplemented(err) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user