forked from toolshed/abra
		
	
		
			
				
	
	
		
			309 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			309 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|    Copyright The containerd Authors.
 | |
| 
 | |
|    Licensed under the Apache License, Version 2.0 (the "License");
 | |
|    you may not use this file except in compliance with the License.
 | |
|    You may obtain a copy of the License at
 | |
| 
 | |
|        http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
|    Unless required by applicable law or agreed to in writing, software
 | |
|    distributed under the License is distributed on an "AS IS" BASIS,
 | |
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|    See the License for the specific language governing permissions and
 | |
|    limitations under the License.
 | |
| */
 | |
| 
 | |
| // 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>|<os>/<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.
 | |
| //
 | |
| // Similarly, the most common arm64 version v8, and most common amd64 version v1
 | |
| // are represented without the variant.
 | |
| //
 | |
| // While these normalizations are provided, their support on arm platforms has
 | |
| // not yet been fully implemented and tested.
 | |
| package platforms
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"path"
 | |
| 	"regexp"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	specifierRe    = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
 | |
| 	osAndVersionRe = regexp.MustCompile(`^([A-Za-z0-9_-]+)(?:\(([A-Za-z0-9_.-]*)\))?$`)
 | |
| )
 | |
| 
 | |
| const osAndVersionFormat = "%s(%s)"
 | |
| 
 | |
| // Platform is a type alias for convenience, so there is no need to import image-spec package everywhere.
 | |
| type Platform = specs.Platform
 | |
| 
 | |
| // Matcher matches platforms specifications, provided by an image or runtime.
 | |
| type Matcher interface {
 | |
| 	Match(platform specs.Platform) bool
 | |
| }
 | |
| 
 | |
| // NewMatcher returns a simple matcher based on the provided platform
 | |
| // specification. The returned matcher only looks for equality based on os,
 | |
| // architecture and variant.
 | |
| //
 | |
| // One may implement their own matcher if this doesn't provide the required
 | |
| // functionality.
 | |
| //
 | |
| // Applications should opt to use `Match` over directly parsing specifiers.
 | |
| func NewMatcher(platform specs.Platform) Matcher {
 | |
| 	return newDefaultMatcher(platform)
 | |
| }
 | |
| 
 | |
| type matcher struct {
 | |
| 	specs.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 FormatAll(m.Platform)
 | |
| }
 | |
| 
 | |
| // ParseAll parses a list of platform specifiers into a list of platform.
 | |
| func ParseAll(specifiers []string) ([]specs.Platform, error) {
 | |
| 	platforms := make([]specs.Platform, len(specifiers))
 | |
| 	for i, s := range specifiers {
 | |
| 		p, err := Parse(s)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("invalid platform %s: %w", s, err)
 | |
| 		}
 | |
| 		platforms[i] = p
 | |
| 	}
 | |
| 	return platforms, nil
 | |
| }
 | |
| 
 | |
| // Parse parses the platform specifier syntax into a platform declaration.
 | |
| //
 | |
| // Platform specifiers are in the format `<os>[(<OSVersion>)]|<arch>|<os>[(<OSVersion>)]/<arch>[/<variant>]`.
 | |
| // The minimum required information for a platform specifier is the operating
 | |
| // system or architecture. The OSVersion can be part of the OS like `windows(10.0.17763)`
 | |
| // When an OSVersion is specified, then specs.Platform.OSVersion is populated with that value,
 | |
| // and an empty string otherwise.
 | |
| // 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(specifier string) (specs.Platform, error) {
 | |
| 	if strings.Contains(specifier, "*") {
 | |
| 		// TODO(stevvooe): need to work out exact wildcard handling
 | |
| 		return specs.Platform{}, fmt.Errorf("%q: wildcards not yet supported: %w", specifier, errInvalidArgument)
 | |
| 	}
 | |
| 
 | |
| 	// Limit to 4 elements to prevent unbounded split
 | |
| 	parts := strings.SplitN(specifier, "/", 4)
 | |
| 
 | |
| 	var p specs.Platform
 | |
| 	for i, part := range parts {
 | |
| 		if i == 0 {
 | |
| 			// First element is <os>[(<OSVersion>)]
 | |
| 			osVer := osAndVersionRe.FindStringSubmatch(part)
 | |
| 			if osVer == nil {
 | |
| 				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)
 | |
| 			}
 | |
| 
 | |
| 			p.OS = normalizeOS(osVer[1])
 | |
| 			p.OSVersion = osVer[2]
 | |
| 		} else {
 | |
| 			if !specifierRe.MatchString(part) {
 | |
| 				return specs.Platform{}, fmt.Errorf("%q is an invalid component of %q: platform specifier component must match %q: %w", part, specifier, specifierRe.String(), errInvalidArgument)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch len(parts) {
 | |
| 	case 1:
 | |
| 		// in this case, we will test that the value might be an OS (with or
 | |
| 		// without the optional OSVersion specified) and 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.
 | |
| 		if isKnownOS(p.OS) {
 | |
| 			// picks a default architecture
 | |
| 			p.Architecture = runtime.GOARCH
 | |
| 			if p.Architecture == "arm" && cpuVariant() != "v7" {
 | |
| 				p.Variant = cpuVariant()
 | |
| 			}
 | |
| 
 | |
| 			return p, nil
 | |
| 		}
 | |
| 
 | |
| 		p.Architecture, p.Variant = normalizeArch(parts[0], "")
 | |
| 		if p.Architecture == "arm" && p.Variant == "v7" {
 | |
| 			p.Variant = ""
 | |
| 		}
 | |
| 		if isKnownArch(p.Architecture) {
 | |
| 			p.OS = runtime.GOOS
 | |
| 			return p, nil
 | |
| 		}
 | |
| 
 | |
| 		return specs.Platform{}, fmt.Errorf("%q: unknown operating system or architecture: %w", specifier, errInvalidArgument)
 | |
| 	case 2:
 | |
| 		// In this case, we treat as a regular OS[(OSVersion)]/arch pair. We don't care
 | |
| 		// about whether or not we know of the platform.
 | |
| 		p.Architecture, p.Variant = normalizeArch(parts[1], "")
 | |
| 		if p.Architecture == "arm" && p.Variant == "v7" {
 | |
| 			p.Variant = ""
 | |
| 		}
 | |
| 
 | |
| 		return p, nil
 | |
| 	case 3:
 | |
| 		// we have a fully specified variant, this is rare
 | |
| 		p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
 | |
| 		if p.Architecture == "arm64" && p.Variant == "" {
 | |
| 			p.Variant = "v8"
 | |
| 		}
 | |
| 
 | |
| 		return p, nil
 | |
| 	}
 | |
| 
 | |
| 	return specs.Platform{}, fmt.Errorf("%q: cannot parse platform specifier: %w", specifier, errInvalidArgument)
 | |
| }
 | |
| 
 | |
| // MustParse is like Parses but panics if the specifier cannot be parsed.
 | |
| // Simplifies initialization of global variables.
 | |
| func MustParse(specifier string) specs.Platform {
 | |
| 	p, err := Parse(specifier)
 | |
| 	if err != nil {
 | |
| 		panic("platform: Parse(" + strconv.Quote(specifier) + "): " + err.Error())
 | |
| 	}
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| // Format returns a string specifier from the provided platform specification.
 | |
| func Format(platform specs.Platform) string {
 | |
| 	if platform.OS == "" {
 | |
| 		return "unknown"
 | |
| 	}
 | |
| 
 | |
| 	return path.Join(platform.OS, platform.Architecture, platform.Variant)
 | |
| }
 | |
| 
 | |
| // FormatAll returns a string specifier that also includes the OSVersion from the
 | |
| // provided platform specification.
 | |
| func FormatAll(platform specs.Platform) string {
 | |
| 	if platform.OS == "" {
 | |
| 		return "unknown"
 | |
| 	}
 | |
| 
 | |
| 	if platform.OSVersion != "" {
 | |
| 		OSAndVersion := fmt.Sprintf(osAndVersionFormat, platform.OS, platform.OSVersion)
 | |
| 		return path.Join(OSAndVersion, platform.Architecture, platform.Variant)
 | |
| 	}
 | |
| 	return path.Join(platform.OS, platform.Architecture, platform.Variant)
 | |
| }
 | |
| 
 | |
| // 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)
 | |
| 
 | |
| 	return platform
 | |
| }
 |