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  }