cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ocimem/writer.go (about) 1 // Copyright 2023 CUE Labs AG 2 // 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 package ocimem 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 22 "cuelabs.dev/go/oci/ociregistry" 23 "cuelabs.dev/go/oci/ociregistry/ociref" 24 "github.com/opencontainers/go-digest" 25 ) 26 27 // This file implements the ociregistry.Writer methods. 28 29 func (r *Registry) PushBlob(ctx context.Context, repoName string, desc ociregistry.Descriptor, content io.Reader) (ociregistry.Descriptor, error) { 30 data, err := io.ReadAll(content) 31 if err != nil { 32 return ociregistry.Descriptor{}, fmt.Errorf("cannot read content: %v", err) 33 } 34 if err := CheckDescriptor(desc, data); err != nil { 35 return ociregistry.Descriptor{}, fmt.Errorf("invalid descriptor: %v", err) 36 } 37 38 r.mu.Lock() 39 defer r.mu.Unlock() 40 repo, err := r.makeRepo(repoName) 41 if err != nil { 42 return ociregistry.Descriptor{}, err 43 } 44 repo.blobs[desc.Digest] = &blob{mediaType: desc.MediaType, data: data} 45 return desc, nil 46 } 47 48 func (r *Registry) PushBlobChunked(ctx context.Context, repoName string, chunkSize int) (ociregistry.BlobWriter, error) { 49 // TODO(mvdan): Why does the ocimem implementation allow a PATCH on an upload ID which doesn't exist? 50 // The tests in ociserver make this assumption, so they break without this bit of code. 51 // 52 // Ideally they should start a new chunked upload to get a new ID, then use that for PATCH/PUT. 53 // Alternatively, add a new method to ocimem outside of the interface to start a chunked upload with a predefined ID. 54 // Either way, this case should be an error, per the spec. 55 return r.PushBlobChunkedResume(ctx, repoName, "", 0, chunkSize) 56 } 57 58 func (r *Registry) PushBlobChunkedResume(ctx context.Context, repoName, id string, offset int64, chunkSize int) (ociregistry.BlobWriter, error) { 59 r.mu.Lock() 60 defer r.mu.Unlock() 61 repo, err := r.makeRepo(repoName) 62 if err != nil { 63 return nil, err 64 } 65 b := repo.uploads[id] 66 if b == nil { 67 b = NewBuffer(func(b *Buffer) error { 68 r.mu.Lock() 69 defer r.mu.Unlock() 70 desc, data, _ := b.GetBlob() 71 repo.blobs[desc.Digest] = &blob{mediaType: desc.MediaType, data: data} 72 return nil 73 }, id) 74 repo.uploads[b.ID()] = b 75 } 76 b.checkStartOffset = offset 77 return b, nil 78 } 79 80 func (r *Registry) MountBlob(ctx context.Context, fromRepo, toRepo string, dig ociregistry.Digest) (ociregistry.Descriptor, error) { 81 r.mu.Lock() 82 defer r.mu.Unlock() 83 rto, err := r.makeRepo(toRepo) 84 if err != nil { 85 return ociregistry.Descriptor{}, err 86 } 87 b, err := r.blobForDigest(fromRepo, dig) 88 if err != nil { 89 return ociregistry.Descriptor{}, err 90 } 91 rto.blobs[dig] = b 92 return b.descriptor(), nil 93 } 94 95 var errCannotOverwriteTag = fmt.Errorf("%w: cannot overwrite tag", ociregistry.ErrDenied) 96 97 func (r *Registry) PushManifest(ctx context.Context, repoName string, tag string, data []byte, mediaType string) (ociregistry.Descriptor, error) { 98 r.mu.Lock() 99 defer r.mu.Unlock() 100 repo, err := r.makeRepo(repoName) 101 if err != nil { 102 return ociregistry.Descriptor{}, err 103 } 104 dig := digest.FromBytes(data) 105 desc := ociregistry.Descriptor{ 106 Digest: dig, 107 MediaType: mediaType, 108 Size: int64(len(data)), 109 } 110 if tag != "" { 111 if !ociref.IsValidTag(tag) { 112 return ociregistry.Descriptor{}, fmt.Errorf("invalid tag") 113 } 114 if r.cfg.ImmutableTags { 115 if currDesc, ok := repo.tags[tag]; ok { 116 if dig == currDesc.Digest { 117 if currDesc.MediaType != mediaType { 118 // Same digest but mismatched media type. 119 return ociregistry.Descriptor{}, fmt.Errorf("%w: mismatched media type", ociregistry.ErrDenied) 120 } 121 // It's got the same content already. Allow it. 122 return currDesc, nil 123 } 124 return ociregistry.Descriptor{}, errCannotOverwriteTag 125 } 126 } 127 } 128 // make a copy of the data to avoid potential corruption. 129 data = append([]byte(nil), data...) 130 if err := CheckDescriptor(desc, data); err != nil { 131 return ociregistry.Descriptor{}, fmt.Errorf("invalid descriptor: %v", err) 132 } 133 subject, err := r.checkManifest(repoName, desc.MediaType, data) 134 if err != nil { 135 return ociregistry.Descriptor{}, fmt.Errorf("invalid manifest: %v", err) 136 } 137 138 repo.manifests[dig] = &blob{ 139 mediaType: mediaType, 140 data: data, 141 subject: subject, 142 } 143 if tag != "" { 144 repo.tags[tag] = desc 145 } 146 return desc, nil 147 } 148 149 func (r *Registry) checkManifest(repoName string, mediaType string, data []byte) (subject ociregistry.Digest, retErr error) { 150 repo, err := r.repo(repoName) 151 if err != nil { 152 return "", err 153 } 154 iter, err := manifestReferences(mediaType, data) 155 if err != nil { 156 // TODO decide what to do about errUnknownManifestMediaTypeForIteration 157 return "", err 158 } 159 iter(func(info descInfo) bool { 160 if err := CheckDescriptor(info.desc, nil); err != nil { 161 retErr = fmt.Errorf("bad descriptor in %s: %v", info.name, err) 162 return false 163 } 164 switch info.kind { 165 case kindBlob: 166 if repo.blobs[info.desc.Digest] == nil { 167 retErr = fmt.Errorf("blob for %s not found", info.name) 168 return false 169 } 170 case kindManifest: 171 if repo.manifests[info.desc.Digest] == nil { 172 retErr = fmt.Errorf("manifest for %s not found", info.name) 173 return false 174 } 175 case kindSubjectManifest: 176 subject = info.desc.Digest 177 // The standard explicitly specifies that we can have 178 // a dangling subject so don't check that it exists. 179 } 180 return true 181 }) 182 return subject, retErr 183 } 184 185 // refersTo reports whether the given digest is referred to, directly or indirectly, by any item 186 // returned by the given iterator, within the given repository. 187 // TODO currently this iterates through all tagged manifests. A better 188 // algorithm could amortise that work and be considerably more efficient. 189 func refersTo(repo *repository, iter descIter, digest ociregistry.Digest) (found bool, retErr error) { 190 iter(func(info descInfo) bool { 191 if info.desc.Digest == digest { 192 found = true 193 return false 194 } 195 switch info.kind { 196 case kindManifest, kindSubjectManifest: 197 b := repo.manifests[info.desc.Digest] 198 if b == nil { 199 break 200 } 201 miter, err := manifestReferences(info.desc.MediaType, b.data) 202 if err != nil { 203 retErr = err 204 return false 205 } 206 found, retErr = refersTo(repo, miter, digest) 207 if found || retErr != nil { 208 return false 209 } 210 } 211 return true 212 }) 213 return found, retErr 214 }