oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/internal/platform/platform.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package platform 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 24 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 25 "oras.land/oras-go/v2/content" 26 "oras.land/oras-go/v2/errdef" 27 "oras.land/oras-go/v2/internal/docker" 28 "oras.land/oras-go/v2/internal/manifestutil" 29 ) 30 31 // Match checks whether the current platform matches the target platform. 32 // Match will return true if all of the following conditions are met. 33 // - Architecture and OS exactly match. 34 // - Variant and OSVersion exactly match if target platform provided. 35 // - OSFeatures of the target platform are the subsets of the OSFeatures 36 // array of the current platform. 37 // 38 // Note: Variant, OSVersion and OSFeatures are optional fields, will skip 39 // the comparison if the target platform does not provide specific value. 40 func Match(got *ocispec.Platform, want *ocispec.Platform) bool { 41 if got == nil && want == nil { 42 return true 43 } 44 45 if got == nil || want == nil { 46 return false 47 } 48 49 if got.Architecture != want.Architecture || got.OS != want.OS { 50 return false 51 } 52 53 if want.OSVersion != "" && got.OSVersion != want.OSVersion { 54 return false 55 } 56 57 if want.Variant != "" && got.Variant != want.Variant { 58 return false 59 } 60 61 if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) { 62 return false 63 } 64 65 return true 66 } 67 68 // isSubset returns true if all items in slice A are present in slice B. 69 func isSubset(a, b []string) bool { 70 set := make(map[string]bool, len(b)) 71 for _, v := range b { 72 set[v] = true 73 } 74 for _, v := range a { 75 if _, ok := set[v]; !ok { 76 return false 77 } 78 } 79 80 return true 81 } 82 83 // SelectManifest implements platform filter and returns the descriptor of the 84 // first matched manifest if the root is a manifest list. If the root is a 85 // manifest, then return the root descriptor if platform matches. 86 func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { 87 switch root.MediaType { 88 case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: 89 manifests, err := manifestutil.Manifests(ctx, src, root) 90 if err != nil { 91 return ocispec.Descriptor{}, err 92 } 93 94 // platform filter 95 for _, m := range manifests { 96 if Match(m.Platform, p) { 97 return m, nil 98 } 99 } 100 return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound) 101 case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: 102 // config will be non-nil for docker manifest and OCI image manifest 103 config, err := manifestutil.Config(ctx, src, root) 104 if err != nil { 105 return ocispec.Descriptor{}, err 106 } 107 108 configMediaType := docker.MediaTypeConfig 109 if root.MediaType == ocispec.MediaTypeImageManifest { 110 configMediaType = ocispec.MediaTypeImageConfig 111 } 112 cfgPlatform, err := getPlatformFromConfig(ctx, src, *config, configMediaType) 113 if err != nil { 114 return ocispec.Descriptor{}, err 115 } 116 117 if Match(cfgPlatform, p) { 118 return root, nil 119 } 120 return ocispec.Descriptor{}, fmt.Errorf("%s: %w: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound) 121 default: 122 return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported) 123 } 124 } 125 126 // getPlatformFromConfig returns a platform object which is made up from the 127 // fields in config blob. 128 func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) { 129 if desc.MediaType != targetConfigMediaType { 130 return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s", desc.MediaType, targetConfigMediaType) 131 } 132 133 rc, err := src.Fetch(ctx, desc) 134 if err != nil { 135 return nil, err 136 } 137 defer rc.Close() 138 139 var platform ocispec.Platform 140 if err = json.NewDecoder(rc).Decode(&platform); err != nil && err != io.EOF { 141 return nil, err 142 } 143 144 return &platform, nil 145 }