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 }