github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/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 }