oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/referrers.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 remote 17 18 import ( 19 "errors" 20 "strings" 21 22 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 23 "oras.land/oras-go/v2/content" 24 "oras.land/oras-go/v2/internal/descriptor" 25 ) 26 27 // zeroDigest represents a digest that consists of zeros. zeroDigest is used 28 // for pinging Referrers API. 29 const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" 30 31 // referrersState represents the state of Referrers API. 32 type referrersState = int32 33 34 const ( 35 // referrersStateUnknown represents an unknown state of Referrers API. 36 referrersStateUnknown referrersState = iota 37 // referrersStateSupported represents that the repository is known to 38 // support Referrers API. 39 referrersStateSupported 40 // referrersStateUnsupported represents that the repository is known to 41 // not support Referrers API. 42 referrersStateUnsupported 43 ) 44 45 // referrerOperation represents an operation on a referrer. 46 type referrerOperation = int32 47 48 const ( 49 // referrerOperationAdd represents an addition operation on a referrer. 50 referrerOperationAdd referrerOperation = iota 51 // referrerOperationRemove represents a removal operation on a referrer. 52 referrerOperationRemove 53 ) 54 55 // referrerChange represents a change on a referrer. 56 type referrerChange struct { 57 referrer ocispec.Descriptor 58 operation referrerOperation 59 } 60 61 var ( 62 // ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability() 63 // when the Referrers API capability has been already set. 64 ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set") 65 66 // errNoReferrerUpdate is returned by applyReferrerChanges() when there 67 // is no any referrer update. 68 errNoReferrerUpdate = errors.New("no referrer update") 69 ) 70 71 const ( 72 // opDeleteReferrersIndex represents the operation for deleting a 73 // referrers index. 74 opDeleteReferrersIndex = "DeleteReferrersIndex" 75 ) 76 77 // ReferrersError records an error and the operation and the subject descriptor. 78 type ReferrersError struct { 79 // Op represents the failing operation. 80 Op string 81 // Subject is the descriptor of referenced artifact. 82 Subject ocispec.Descriptor 83 // Err is the entity of referrers error. 84 Err error 85 } 86 87 // Error returns error msg of IgnorableError. 88 func (e *ReferrersError) Error() string { 89 return e.Err.Error() 90 } 91 92 // Unwrap returns the inner error of IgnorableError. 93 func (e *ReferrersError) Unwrap() error { 94 return errors.Unwrap(e.Err) 95 } 96 97 // IsIndexDelete tells if e is kind of error related to referrers 98 // index deletion. 99 func (e *ReferrersError) IsReferrersIndexDelete() bool { 100 return e.Op == opDeleteReferrersIndex 101 } 102 103 // buildReferrersTag builds the referrers tag for the given manifest descriptor. 104 // Format: <algorithm>-<digest> 105 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#unavailable-referrers-api 106 func buildReferrersTag(desc ocispec.Descriptor) string { 107 alg := desc.Digest.Algorithm().String() 108 encoded := desc.Digest.Encoded() 109 return alg + "-" + encoded 110 } 111 112 // isReferrersFilterApplied checks if requsted is in the applied filter list. 113 func isReferrersFilterApplied(applied, requested string) bool { 114 if applied == "" || requested == "" { 115 return false 116 } 117 filters := strings.Split(applied, ",") 118 for _, f := range filters { 119 if f == requested { 120 return true 121 } 122 } 123 return false 124 } 125 126 // filterReferrers filters a slice of referrers by artifactType in place. 127 // The returned slice contains matching referrers. 128 func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor { 129 if artifactType == "" { 130 return refs 131 } 132 var j int 133 for i, ref := range refs { 134 if ref.ArtifactType == artifactType { 135 if i != j { 136 refs[j] = ref 137 } 138 j++ 139 } 140 } 141 return refs[:j] 142 } 143 144 // applyReferrerChanges applies referrerChanges on referrers and returns the 145 // updated referrers. 146 // Returns errNoReferrerUpdate if there is no any referrers updates. 147 func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) { 148 referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges)) 149 updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges)) 150 var updateRequired bool 151 for _, r := range referrers { 152 if content.Equal(r, ocispec.Descriptor{}) { 153 // skip bad entry 154 updateRequired = true 155 continue 156 } 157 key := descriptor.FromOCI(r) 158 if _, ok := referrersMap[key]; ok { 159 // skip duplicates 160 updateRequired = true 161 continue 162 } 163 updatedReferrers = append(updatedReferrers, r) 164 referrersMap[key] = len(updatedReferrers) - 1 165 } 166 167 // apply changes 168 for _, change := range referrerChanges { 169 key := descriptor.FromOCI(change.referrer) 170 switch change.operation { 171 case referrerOperationAdd: 172 if _, ok := referrersMap[key]; !ok { 173 // add distinct referrers 174 updatedReferrers = append(updatedReferrers, change.referrer) 175 referrersMap[key] = len(updatedReferrers) - 1 176 } 177 case referrerOperationRemove: 178 if pos, ok := referrersMap[key]; ok { 179 // remove referrers that are already in the map 180 updatedReferrers[pos] = ocispec.Descriptor{} 181 delete(referrersMap, key) 182 } 183 } 184 } 185 186 // skip unnecessary update 187 if !updateRequired && len(referrersMap) == len(referrers) { 188 // if the result referrer map contains the same content as the 189 // original referrers, consider that there is no update on the 190 // referrers. 191 for _, r := range referrers { 192 key := descriptor.FromOCI(r) 193 if _, ok := referrersMap[key]; !ok { 194 updateRequired = true 195 } 196 } 197 if !updateRequired { 198 return nil, errNoReferrerUpdate 199 } 200 } 201 202 return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil 203 } 204 205 // removeEmptyDescriptors in-place removes empty items from descs, given a hint 206 // of the number of non-empty descriptors. 207 func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor { 208 j := 0 209 for i, r := range descs { 210 if !content.Equal(r, ocispec.Descriptor{}) { 211 if i > j { 212 descs[j] = r 213 } 214 j++ 215 } 216 if j == hint { 217 break 218 } 219 } 220 return descs[:j] 221 }