github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/oci/casext/refname.go (about)

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