github.com/containerd/Containerd@v1.4.13/platforms/platforms.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  // Package platforms provides a toolkit for normalizing, matching and
    18  // specifying container platforms.
    19  //
    20  // Centered around OCI platform specifications, we define a string-based
    21  // specifier syntax that can be used for user input. With a specifier, users
    22  // only need to specify the parts of the platform that are relevant to their
    23  // context, providing an operating system or architecture or both.
    24  //
    25  // How do I use this package?
    26  //
    27  // The vast majority of use cases should simply use the match function with
    28  // user input. The first step is to parse a specifier into a matcher:
    29  //
    30  //   m, err := Parse("linux")
    31  //   if err != nil { ... }
    32  //
    33  // Once you have a matcher, use it to match against the platform declared by a
    34  // component, typically from an image or runtime. Since extracting an images
    35  // platform is a little more involved, we'll use an example against the
    36  // platform default:
    37  //
    38  //   if ok := m.Match(Default()); !ok { /* doesn't match */ }
    39  //
    40  // This can be composed in loops for resolving runtimes or used as a filter for
    41  // fetch and select images.
    42  //
    43  // More details of the specifier syntax and platform spec follow.
    44  //
    45  // Declaring Platform Support
    46  //
    47  // Components that have strict platform requirements should use the OCI
    48  // platform specification to declare their support. Typically, this will be
    49  // images and runtimes that should make these declaring which platform they
    50  // support specifically. This looks roughly as follows:
    51  //
    52  //   type Platform struct {
    53  //	   Architecture string
    54  //	   OS           string
    55  //	   Variant      string
    56  //   }
    57  //
    58  // Most images and runtimes should at least set Architecture and OS, according
    59  // to their GOARCH and GOOS values, respectively (follow the OCI image
    60  // specification when in doubt). ARM should set variant under certain
    61  // discussions, which are outlined below.
    62  //
    63  // Platform Specifiers
    64  //
    65  // While the OCI platform specifications provide a tool for components to
    66  // specify structured information, user input typically doesn't need the full
    67  // context and much can be inferred. To solve this problem, we introduced
    68  // "specifiers". A specifier has the format
    69  // `<os>|<arch>|<os>/<arch>[/<variant>]`.  The user can provide either the
    70  // operating system or the architecture or both.
    71  //
    72  // An example of a common specifier is `linux/amd64`. If the host has a default
    73  // of runtime that matches this, the user can simply provide the component that
    74  // matters. For example, if a image provides amd64 and arm64 support, the
    75  // operating system, `linux` can be inferred, so they only have to provide
    76  // `arm64` or `amd64`. Similar behavior is implemented for operating systems,
    77  // where the architecture may be known but a runtime may support images from
    78  // different operating systems.
    79  //
    80  // Normalization
    81  //
    82  // Because not all users are familiar with the way the Go runtime represents
    83  // platforms, several normalizations have been provided to make this package
    84  // easier to user.
    85  //
    86  // The following are performed for architectures:
    87  //
    88  //   Value    Normalized
    89  //   aarch64  arm64
    90  //   armhf    arm
    91  //   armel    arm/v6
    92  //   i386     386
    93  //   x86_64   amd64
    94  //   x86-64   amd64
    95  //
    96  // We also normalize the operating system `macos` to `darwin`.
    97  //
    98  // ARM Support
    99  //
   100  // To qualify ARM architecture, the Variant field is used to qualify the arm
   101  // version. The most common arm version, v7, is represented without the variant
   102  // unless it is explicitly provided. This is treated as equivalent to armhf. A
   103  // previous architecture, armel, will be normalized to arm/v6.
   104  //
   105  // While these normalizations are provided, their support on arm platforms has
   106  // not yet been fully implemented and tested.
   107  package platforms
   108  
   109  import (
   110  	"regexp"
   111  	"runtime"
   112  	"strconv"
   113  	"strings"
   114  
   115  	"github.com/containerd/containerd/errdefs"
   116  	specs "github.com/opencontainers/image-spec/specs-go/v1"
   117  	"github.com/pkg/errors"
   118  )
   119  
   120  var (
   121  	specifierRe = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
   122  )
   123  
   124  // Matcher matches platforms specifications, provided by an image or runtime.
   125  type Matcher interface {
   126  	Match(platform specs.Platform) bool
   127  }
   128  
   129  // NewMatcher returns a simple matcher based on the provided platform
   130  // specification. The returned matcher only looks for equality based on os,
   131  // architecture and variant.
   132  //
   133  // One may implement their own matcher if this doesn't provide the required
   134  // functionality.
   135  //
   136  // Applications should opt to use `Match` over directly parsing specifiers.
   137  func NewMatcher(platform specs.Platform) Matcher {
   138  	return &matcher{
   139  		Platform: Normalize(platform),
   140  	}
   141  }
   142  
   143  type matcher struct {
   144  	specs.Platform
   145  }
   146  
   147  func (m *matcher) Match(platform specs.Platform) bool {
   148  	normalized := Normalize(platform)
   149  	return m.OS == normalized.OS &&
   150  		m.Architecture == normalized.Architecture &&
   151  		m.Variant == normalized.Variant
   152  }
   153  
   154  func (m *matcher) String() string {
   155  	return Format(m.Platform)
   156  }
   157  
   158  // Parse parses the platform specifier syntax into a platform declaration.
   159  //
   160  // Platform specifiers are in the format `<os>|<arch>|<os>/<arch>[/<variant>]`.
   161  // The minimum required information for a platform specifier is the operating
   162  // system or architecture. If there is only a single string (no slashes), the
   163  // value will be matched against the known set of operating systems, then fall
   164  // back to the known set of architectures. The missing component will be
   165  // inferred based on the local environment.
   166  func Parse(specifier string) (specs.Platform, error) {
   167  	if strings.Contains(specifier, "*") {
   168  		// TODO(stevvooe): need to work out exact wildcard handling
   169  		return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: wildcards not yet supported", specifier)
   170  	}
   171  
   172  	parts := strings.Split(specifier, "/")
   173  
   174  	for _, part := range parts {
   175  		if !specifierRe.MatchString(part) {
   176  			return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q is an invalid component of %q: platform specifier component must match %q", part, specifier, specifierRe.String())
   177  		}
   178  	}
   179  
   180  	var p specs.Platform
   181  	switch len(parts) {
   182  	case 1:
   183  		// in this case, we will test that the value might be an OS, then look
   184  		// it up. If it is not known, we'll treat it as an architecture. Since
   185  		// we have very little information about the platform here, we are
   186  		// going to be a little more strict if we don't know about the argument
   187  		// value.
   188  		p.OS = normalizeOS(parts[0])
   189  		if isKnownOS(p.OS) {
   190  			// picks a default architecture
   191  			p.Architecture = runtime.GOARCH
   192  			if p.Architecture == "arm" && cpuVariant != "v7" {
   193  				p.Variant = cpuVariant
   194  			}
   195  
   196  			return p, nil
   197  		}
   198  
   199  		p.Architecture, p.Variant = normalizeArch(parts[0], "")
   200  		if p.Architecture == "arm" && p.Variant == "v7" {
   201  			p.Variant = ""
   202  		}
   203  		if isKnownArch(p.Architecture) {
   204  			p.OS = runtime.GOOS
   205  			return p, nil
   206  		}
   207  
   208  		return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: unknown operating system or architecture", specifier)
   209  	case 2:
   210  		// In this case, we treat as a regular os/arch pair. We don't care
   211  		// about whether or not we know of the platform.
   212  		p.OS = normalizeOS(parts[0])
   213  		p.Architecture, p.Variant = normalizeArch(parts[1], "")
   214  		if p.Architecture == "arm" && p.Variant == "v7" {
   215  			p.Variant = ""
   216  		}
   217  
   218  		return p, nil
   219  	case 3:
   220  		// we have a fully specified variant, this is rare
   221  		p.OS = normalizeOS(parts[0])
   222  		p.Architecture, p.Variant = normalizeArch(parts[1], parts[2])
   223  		if p.Architecture == "arm64" && p.Variant == "" {
   224  			p.Variant = "v8"
   225  		}
   226  
   227  		return p, nil
   228  	}
   229  
   230  	return specs.Platform{}, errors.Wrapf(errdefs.ErrInvalidArgument, "%q: cannot parse platform specifier", specifier)
   231  }
   232  
   233  // MustParse is like Parses but panics if the specifier cannot be parsed.
   234  // Simplifies initialization of global variables.
   235  func MustParse(specifier string) specs.Platform {
   236  	p, err := Parse(specifier)
   237  	if err != nil {
   238  		panic("platform: Parse(" + strconv.Quote(specifier) + "): " + err.Error())
   239  	}
   240  	return p
   241  }
   242  
   243  // Format returns a string specifier from the provided platform specification.
   244  func Format(platform specs.Platform) string {
   245  	if platform.OS == "" {
   246  		return "unknown"
   247  	}
   248  
   249  	return joinNotEmpty(platform.OS, platform.Architecture, platform.Variant)
   250  }
   251  
   252  func joinNotEmpty(s ...string) string {
   253  	var ss []string
   254  	for _, s := range s {
   255  		if s == "" {
   256  			continue
   257  		}
   258  
   259  		ss = append(ss, s)
   260  	}
   261  
   262  	return strings.Join(ss, "/")
   263  }
   264  
   265  // Normalize validates and translate the platform to the canonical value.
   266  //
   267  // For example, if "Aarch64" is encountered, we change it to "arm64" or if
   268  // "x86_64" is encountered, it becomes "amd64".
   269  func Normalize(platform specs.Platform) specs.Platform {
   270  	platform.OS = normalizeOS(platform.OS)
   271  	platform.Architecture, platform.Variant = normalizeArch(platform.Architecture, platform.Variant)
   272  
   273  	// these fields are deprecated, remove them
   274  	platform.OSFeatures = nil
   275  	platform.OSVersion = ""
   276  
   277  	return platform
   278  }