github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/casext/refname.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package casext
    19  
    20  import (
    21  	"context"
    22  	"regexp"
    23  
    24  	"github.com/apex/log"
    25  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    26  	"github.com/opencontainers/umoci/oci/casext/mediatype"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  // refnameRegex is a regex that only matches reference names that are valid
    31  // according to the OCI specification. See IsValidReferenceName for the EBNF.
    32  var refnameRegex = regexp.MustCompile(`^([A-Za-z0-9]+(([-._:@+]|--)[A-Za-z0-9]+)*)(/([A-Za-z0-9]+(([-._:@+]|--)[A-Za-z0-9]+)*))*$`)
    33  
    34  // IsValidReferenceName returns whether the provided annotation value for
    35  // "org.opencontainers.image.ref.name" is actually valid according to the
    36  // OCI specification. This only matches against the MUST requirement, not the
    37  // SHOULD requirement. The EBNF defined in the specification is:
    38  //
    39  //	refname   ::= component ("/" component)*
    40  //	component ::= alphanum (separator alphanum)*
    41  //	alphanum  ::= [A-Za-z0-9]+
    42  //	separator ::= [-._:@+] | "--"
    43  func IsValidReferenceName(refname string) bool {
    44  	return refnameRegex.MatchString(refname)
    45  }
    46  
    47  // ResolveReference will attempt to resolve all possible descriptor paths to
    48  // Manifests (or any unknown blobs) that match a particular reference name (if
    49  // descriptors are stored in non-standard blobs, Resolve will be unable to find
    50  // them but will return the top-most unknown descriptor).
    51  // ResolveReference assumes that "reference name" refers to the value of the
    52  // "org.opencontainers.image.ref.name" descriptor annotation. It is recommended
    53  // that if the returned slice of descriptors is greater than zero that the user
    54  // be consulted to resolve the conflict (due to ambiguity in resolution paths).
    55  //
    56  // TODO: How are we meant to implement other restrictions such as the
    57  //
    58  //	architecture and feature flags? The API will need to change.
    59  func (e Engine) ResolveReference(ctx context.Context, refname string) ([]DescriptorPath, error) {
    60  	// XXX: It should be possible to override this somehow, in case we are
    61  	//      dealing with an image that abuses the image specification in some
    62  	//      way.
    63  	if !IsValidReferenceName(refname) {
    64  		return nil, errors.Errorf("refusing to resolve invalid reference %q", refname)
    65  	}
    66  
    67  	index, err := e.GetIndex(ctx)
    68  	if err != nil {
    69  		return nil, errors.Wrap(err, "get top-level index")
    70  	}
    71  
    72  	// Set of root links that match the given refname.
    73  	var roots []ispec.Descriptor
    74  
    75  	// We only consider the case where AnnotationRefName is defined on the
    76  	// top-level of the index tree. While this isn't codified in the spec (at
    77  	// the time of writing -- 1.0.0-rc5) there are some discussions to add this
    78  	// restriction in 1.0.0-rc6.
    79  	for _, descriptor := range index.Manifests {
    80  		// XXX: What should we do if refname == "".
    81  		if descriptor.Annotations[ispec.AnnotationRefName] == refname {
    82  			roots = append(roots, descriptor)
    83  		}
    84  	}
    85  
    86  	// The resolved set of descriptors.
    87  	var resolutions []DescriptorPath
    88  	for _, root := range roots {
    89  		// Find all manifests or other blobs that are reachable from the given
    90  		// descriptor.
    91  		if err := e.Walk(ctx, root, func(descriptorPath DescriptorPath) error {
    92  			descriptor := descriptorPath.Descriptor()
    93  
    94  			// If the media-type should be treated as a "target media-type" for
    95  			// reference resolution, we stop resolution here and add it to the
    96  			// set of resolved paths.
    97  			if mediatype.IsTarget(descriptor.MediaType) {
    98  				resolutions = append(resolutions, descriptorPath)
    99  				return ErrSkipDescriptor
   100  			}
   101  			return nil
   102  		}); err != nil {
   103  			return nil, errors.Wrapf(err, "walk %s", root.Digest)
   104  		}
   105  	}
   106  
   107  	log.WithFields(log.Fields{
   108  		"refs": resolutions,
   109  	}).Debugf("casext.ResolveReference(%s) got these descriptors", refname)
   110  	return resolutions, nil
   111  }
   112  
   113  // XXX: Should the *Reference set of interfaces support DescriptorPath? While
   114  //      it might seem like it doesn't make sense, a DescriptorPath entirely
   115  //      removes ambiguity with regards to which root needs to be operated on.
   116  //      If a user has that information we should provide them a way to use it.
   117  
   118  // UpdateReference replaces an existing entry for refname with the given
   119  // descriptor. If there are multiple descriptors that match the refname they
   120  // are all replaced with the given descriptor.
   121  func (e Engine) UpdateReference(ctx context.Context, refname string, descriptor ispec.Descriptor) error {
   122  	// XXX: It should be possible to override this somehow, in case we are
   123  	//      dealing with an image that abuses the image specification in some
   124  	//      way.
   125  	if !IsValidReferenceName(refname) {
   126  		return errors.Errorf("refusing to update invalid reference %q", refname)
   127  	}
   128  
   129  	// Get index to modify.
   130  	index, err := e.GetIndex(ctx)
   131  	if err != nil {
   132  		return errors.Wrap(err, "get top-level index")
   133  	}
   134  
   135  	// TODO: Handle refname = "".
   136  	var newIndex []ispec.Descriptor
   137  	for _, descriptor := range index.Manifests {
   138  		if descriptor.Annotations[ispec.AnnotationRefName] != refname {
   139  			newIndex = append(newIndex, descriptor)
   140  		}
   141  	}
   142  	if len(newIndex)-len(index.Manifests) > 1 {
   143  		// Warn users if the operation is going to remove more than one references.
   144  		log.Warn("multiple references match the given reference name -- all of them have been replaced due to this ambiguity")
   145  	}
   146  
   147  	// Append the descriptor.
   148  	if descriptor.Annotations == nil {
   149  		descriptor.Annotations = map[string]string{}
   150  	}
   151  	descriptor.Annotations[ispec.AnnotationRefName] = refname
   152  	newIndex = append(newIndex, descriptor)
   153  
   154  	// Commit to image.
   155  	index.Manifests = newIndex
   156  	if err := e.PutIndex(ctx, index); err != nil {
   157  		return errors.Wrap(err, "replace index")
   158  	}
   159  	return nil
   160  }
   161  
   162  // DeleteReference removes all entries in the index that match the given
   163  // refname.
   164  func (e Engine) DeleteReference(ctx context.Context, refname string) error {
   165  	// XXX: It should be possible to override this somehow, in case we are
   166  	//      dealing with an image that abuses the image specification in some
   167  	//      way.
   168  	if !IsValidReferenceName(refname) {
   169  		return errors.Errorf("refusing to delete invalid reference %q", refname)
   170  	}
   171  
   172  	// Get index to modify.
   173  	index, err := e.GetIndex(ctx)
   174  	if err != nil {
   175  		return errors.Wrap(err, "get top-level index")
   176  	}
   177  
   178  	// TODO: Handle refname = "".
   179  	var newIndex []ispec.Descriptor
   180  	for _, descriptor := range index.Manifests {
   181  		if descriptor.Annotations[ispec.AnnotationRefName] != refname {
   182  			newIndex = append(newIndex, descriptor)
   183  		}
   184  	}
   185  	if len(newIndex)-len(index.Manifests) > 1 {
   186  		// Warn users if the operation is going to remove more than one references.
   187  		log.Warn("multiple references match the given reference name -- all of them have been deleted due to this ambiguity")
   188  	}
   189  
   190  	// Commit to image.
   191  	index.Manifests = newIndex
   192  	if err := e.PutIndex(ctx, index); err != nil {
   193  		return errors.Wrap(err, "replace index")
   194  	}
   195  	return nil
   196  }
   197  
   198  // ListReferences returns all of the ref.name entries that are specified in the
   199  // top-level index. Note that the list may contain duplicates, due to the
   200  // nature of references in the image-spec.
   201  func (e Engine) ListReferences(ctx context.Context) ([]string, error) {
   202  	// Get index.
   203  	index, err := e.GetIndex(ctx)
   204  	if err != nil {
   205  		return nil, errors.Wrap(err, "get top-level index")
   206  	}
   207  
   208  	var refs []string
   209  	for _, descriptor := range index.Manifests {
   210  		ref, ok := descriptor.Annotations[ispec.AnnotationRefName]
   211  		if ok {
   212  			refs = append(refs, ref)
   213  		}
   214  	}
   215  	return refs, nil
   216  }