github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/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 "github.com/opcr-io/oras-go/v2/content" 25 "github.com/opcr-io/oras-go/v2/errdef" 26 "github.com/opcr-io/oras-go/v2/internal/docker" 27 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 28 ) 29 30 // Match checks whether the current platform matches the target platform. 31 // Match will return true if all of the following conditions are met. 32 // - Architecture and OS exactly match. 33 // - Variant and OSVersion exactly match if target platform provided. 34 // - OSFeatures of the target platform are the subsets of the OSFeatures 35 // array of the current platform. 36 // 37 // Note: Variant, OSVersion and OSFeatures are optional fields, will skip 38 // the comparison if the target platform does not provide specfic value. 39 func Match(got *ocispec.Platform, want *ocispec.Platform) bool { 40 if got.Architecture != want.Architecture || got.OS != want.OS { 41 return false 42 } 43 44 if want.OSVersion != "" && got.OSVersion != want.OSVersion { 45 return false 46 } 47 48 if want.Variant != "" && got.Variant != want.Variant { 49 return false 50 } 51 52 if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) { 53 return false 54 } 55 56 return true 57 } 58 59 // isSubset returns true if all items in slice A are present in slice B. 60 func isSubset(a, b []string) bool { 61 set := make(map[string]bool, len(b)) 62 for _, v := range b { 63 set[v] = true 64 } 65 for _, v := range a { 66 if _, ok := set[v]; !ok { 67 return false 68 } 69 } 70 71 return true 72 } 73 74 // SelectManifest implements platform filter and returns the descriptor of the 75 // first matched manifest if the root is a manifest list. If the root is a 76 // manifest, then return the root descriptor if platform matches. 77 func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { 78 switch root.MediaType { 79 case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: 80 manifests, err := content.Successors(ctx, src, root) 81 if err != nil { 82 return ocispec.Descriptor{}, err 83 } 84 85 // platform filter 86 for _, m := range manifests { 87 if Match(m.Platform, p) { 88 return m, nil 89 } 90 } 91 return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound) 92 case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: 93 descs, err := content.Successors(ctx, src, root) 94 if err != nil { 95 return ocispec.Descriptor{}, err 96 } 97 98 configMediaType := docker.MediaTypeConfig 99 if root.MediaType == ocispec.MediaTypeImageManifest { 100 configMediaType = ocispec.MediaTypeImageConfig 101 } 102 103 cfgPlatform, err := getPlatformFromConfig(ctx, src, descs[0], configMediaType) 104 if err != nil { 105 return ocispec.Descriptor{}, err 106 } 107 108 if Match(cfgPlatform, p) { 109 return root, nil 110 } 111 return ocispec.Descriptor{}, fmt.Errorf("%s: %w: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound) 112 default: 113 return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported) 114 } 115 } 116 117 // getPlatformFromConfig returns a platform object which is made up from the 118 // fields in config blob. 119 func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) { 120 if desc.MediaType != targetConfigMediaType { 121 return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s", desc.MediaType, targetConfigMediaType) 122 } 123 124 rc, err := src.Fetch(ctx, desc) 125 if err != nil { 126 return nil, err 127 } 128 defer rc.Close() 129 130 var platform ocispec.Platform 131 if err = json.NewDecoder(rc).Decode(&platform); err != nil && err != io.EOF { 132 return nil, err 133 } 134 135 return &platform, nil 136 }