diff --git a/differ/differ.go b/differ/differ.go index 60e5d3eea..38d6aec85 100644 --- a/differ/differ.go +++ b/differ/differ.go @@ -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") { isCompressed = true } 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 isCompressed = true 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-") if err != nil { diff --git a/errdefs/errors.go b/errdefs/errors.go index c8f0a6c46..44ec72e7b 100644 --- a/errdefs/errors.go +++ b/errdefs/errors.go @@ -26,7 +26,7 @@ var ( ErrAlreadyExists = errors.New("already exists") ErrFailedPrecondition = errors.New("failed precondition") 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 { @@ -54,6 +54,6 @@ func IsUnavailable(err error) bool { return errors.Cause(err) == ErrUnavailable } -func IsNotSupported(err error) bool { - return errors.Cause(err) == ErrNotSupported +func IsNotImplemented(err error) bool { + return errors.Cause(err) == ErrNotImplemented } diff --git a/errdefs/grpc.go b/errdefs/grpc.go index 53e7ee2d4..76fb3eb3f 100644 --- a/errdefs/grpc.go +++ b/errdefs/grpc.go @@ -38,7 +38,7 @@ func ToGRPC(err error) error { return grpc.Errorf(codes.FailedPrecondition, err.Error()) case IsUnavailable(err): return grpc.Errorf(codes.Unavailable, err.Error()) - case IsNotSupported(err): + case IsNotImplemented(err): return grpc.Errorf(codes.Unimplemented, err.Error()) } @@ -72,7 +72,7 @@ func FromGRPC(err error) error { case codes.FailedPrecondition: cls = ErrFailedPrecondition case codes.Unimplemented: - cls = ErrNotSupported + cls = ErrNotImplemented default: cls = ErrUnknown } diff --git a/platforms/platforms.go b/platforms/platforms.go index 96ecf38ec..95c40d7f9 100644 --- a/platforms/platforms.go +++ b/platforms/platforms.go @@ -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 `[/[/]]`. +// 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 import ( @@ -10,36 +99,56 @@ import ( "github.com/pkg/errors" ) -type platformKey struct { - os string - arch string - variant string -} - var ( - selectorRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) ) -// ParseSelector parses the platform selector syntax into a platform -// declaration. +// Matcher matches platforms specifications, provided by an image or runtime. +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 [/[/]]. The -// minimum required information for a platform selector is the operating system +// Platform specifiers are in the format [/[/]]. The +// minimum required information for a platform specifier 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, "*") { +// +// 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 - 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 { - 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()) + if !specifierRe.MatchString(part) { + 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 if p.Architecture == "arm" { // 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], "") if isKnownArch(p.Architecture) { 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: // 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 + return &matcher{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 &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 { - return false -} - -// Format returns a string that provides a shortened overview of the platform. +// Format returns a string specifier from the provided platform specification. func Format(platform specs.Platform) string { if platform.OS == "" { return "unknown" diff --git a/platforms/platforms_test.go b/platforms/platforms_test.go index a34fa4753..a88dfbf42 100644 --- a/platforms/platforms_test.go +++ b/platforms/platforms_test.go @@ -1,6 +1,7 @@ package platforms import ( + "fmt" "reflect" "runtime" "testing" @@ -174,16 +175,25 @@ func TestParseSelector(t *testing.T) { if testcase.skip { t.Skip("this case is not yet supported") } - p, err := Parse(testcase.input) + m, 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) + if !reflect.DeepEqual(m.Spec(), 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 { 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) } - if Format(reparsed) != formatted { - t.Fatalf("normalized output did not survive the round trip: %v != %v", Format(reparsed), formatted) + if Format(reparsed.Spec()) != 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/foo/bar", // too mayn components + input: "linux/arm/foo/bar", // too many components }, } { t.Run(testcase.input, func(t *testing.T) { diff --git a/services/diff/service.go b/services/diff/service.go index 6d65fd951..af6326b60 100644 --- a/services/diff/service.go +++ b/services/diff/service.go @@ -74,7 +74,7 @@ func (s *service) Apply(ctx context.Context, er *diffapi.ApplyRequest) (*diffapi for _, differ := range s.differs { ocidesc, err = differ.Apply(ctx, desc, mounts) - if !errdefs.IsNotSupported(err) { + if !errdefs.IsNotImplemented(err) { break } } @@ -99,7 +99,7 @@ func (s *service) Diff(ctx context.Context, dr *diffapi.DiffRequest) (*diffapi.D for _, differ := range s.differs { ocidesc, err = differ.DiffMounts(ctx, aMounts, bMounts, dr.MediaType, dr.Ref) - if !errdefs.IsNotSupported(err) { + if !errdefs.IsNotImplemented(err) { break } }