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  }