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 ociregistry provides an abstraction that represents the 16 // capabilities provided by an OCI registry. 17 // 18 // See the [OCI distribution specification] for more information on OCI registries. 19 // 20 // Packages within this module provide the capability to translate to and 21 // from the HTTP protocol documented in that specification: 22 // - [cuelabs.dev/go/oci/ociregistry/ociclient] provides an [Interface] value 23 // that acts as an HTTP client. 24 // - [cuelabs.dev/go/oci/ociregistry/ociserver] provides an HTTP server 25 // that serves the distribution protocol by making calls to an arbitrary 26 // [Interface] value. 27 // 28 // When used together in a stack, the above two packages can be used 29 // to provide a simple proxy server. 30 // 31 // The [cuelabs.dev/go/oci/ociregistry/ocimem] package provides a trivial 32 // in-memory implementation of the interface. 33 // 34 // Other packages provide some utilities that manipulate [Interface] values: 35 // - [cuelabs.dev/go/oci/ociregistry/ocifilter] provides functionality for exposing 36 // modified or restricted views onto a registry. 37 // - [cuelabs.dev/go/oci/ociregistry/ociunify] can combine two registries into one 38 // unified view across both. 39 // 40 // # Notes on [Interface] 41 // 42 // In general, the caller cannot assume that the implementation of a given [Interface] value 43 // is present on the network. For example, [cuelabs.dev/go/oci/ociregistry/ocimem] 44 // doesn't know about the network at all. But there are times when an implementation 45 // might want to provide information about the location of blobs or manifests so 46 // that a client can go direct if it wishes. That is, a proxy might not wish 47 // to ship all the traffic for all blobs through itself, but instead redirect clients 48 // to talk to some other location on the internet. 49 // 50 // When an [Interface] implementation wishes to provide that information, it 51 // can do so by setting the `URLs` field on the descriptor that it returns for 52 // a given blob or manifest. Although it is not mandatory for a caller to use 53 // this, some callers (specifically the ociserver package) can use this information 54 // to redirect clients appropriately. 55 // 56 // [OCI distribution specification]: https://github.com/opencontainers/distribution-spec/blob/main/spec.md 57 package ociregistry 58 59 import ( 60 "context" 61 "io" 62 63 "github.com/opencontainers/go-digest" 64 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 65 ) 66 67 // Interface defines a generic interface to a single OCI registry. 68 // It does not support cross-registry operations: all methods are 69 // directed to the receiver only. 70 type Interface interface { 71 Writer 72 Reader 73 Deleter 74 Lister 75 private() 76 } 77 78 type ReadWriter interface { 79 Reader 80 Writer 81 } 82 83 type ( 84 Digest = digest.Digest 85 Descriptor = ocispec.Descriptor 86 Manifest = ocispec.Manifest 87 ) 88 89 type Reader interface { 90 // GetBlob returns the content of the blob with the given digest. 91 // The context also controls the lifetime of the returned BlobReader. 92 // Errors: 93 // - ErrNameUnknown when the repository is not present. 94 // - ErrBlobUnknown when the blob is not present in the repository. 95 GetBlob(ctx context.Context, repo string, digest Digest) (BlobReader, error) 96 97 // GetBlobRange is like GetBlob but asks to get only the given range of bytes from the blob, 98 // starting at offset0, up to but not including offset1. 99 // If offset1 is negative or exceeds the actual size of the blob, GetBlobRange will 100 // return all the data starting from offset0. 101 // The context also controls the lifetime of the returned BlobReader. 102 GetBlobRange(ctx context.Context, repo string, digest Digest, offset0, offset1 int64) (BlobReader, error) 103 104 // GetManifest returns the contents of the manifest with the given digest. 105 // The context also controls the lifetime of the returned BlobReader. 106 // Errors: 107 // - ErrNameUnknown when the repository is not present. 108 // - ErrManifestUnknown when the blob is not present in the repository. 109 GetManifest(ctx context.Context, repo string, digest Digest) (BlobReader, error) 110 111 // GetTag returns the contents of the manifest with the given tag. 112 // The context also controls the lifetime of the returned BlobReader. 113 // Errors: 114 // - ErrNameUnknown when the repository is not present. 115 // - ErrManifestUnknown when the tag is not present in the repository. 116 GetTag(ctx context.Context, repo string, tagName string) (BlobReader, error) 117 118 // ResolveDigest returns the descriptor for a given blob. 119 // Only the MediaType, Digest and Size fields will be filled out. 120 // Errors: 121 // - ErrNameUnknown when the repository is not present. 122 // - ErrBlobUnknown when the blob is not present in the repository. 123 ResolveBlob(ctx context.Context, repo string, digest Digest) (Descriptor, error) 124 125 // ResolveManifest returns the descriptor for a given maniifest. 126 // Only the MediaType, Digest and Size fields will be filled out. 127 // Errors: 128 // - ErrNameUnknown when the repository is not present. 129 // - ErrManifestUnknown when the blob is not present in the repository. 130 ResolveManifest(ctx context.Context, repo string, digest Digest) (Descriptor, error) 131 132 // ResolveTag returns the descriptor for a given tag. 133 // Only the MediaType, Digest and Size fields will be filled out. 134 // Errors: 135 // - ErrNameUnknown when the repository is not present. 136 // - ErrManifestUnknown when the blob is not present in the repository. 137 ResolveTag(ctx context.Context, repo string, tagName string) (Descriptor, error) 138 } 139 140 // Writer defines registry actions that write to blobs, manifests and tags. 141 type Writer interface { 142 // PushBlob pushes a blob described by desc to the given repository, reading content from r. 143 // Only the desc.Digest and desc.Size fields are used. 144 // It returns desc with Digest set to the canonical digest for the blob. 145 // Errors: 146 // - ErrNameUnknown when the repository is not present. 147 // - ErrNameInvalid when the repository name is not valid. 148 // - ErrDigestInvalid when desc.Digest does not match the content. 149 // - ErrSizeInvalid when desc.Size does not match the content length. 150 PushBlob(ctx context.Context, repo string, desc Descriptor, r io.Reader) (Descriptor, error) 151 152 // PushBlobChunked starts to push a blob to the given repository. 153 // The returned [BlobWriter] can be used to stream the upload and resume on temporary errors. 154 // 155 // The chunkSize parameter provides a hint for the chunk size to use 156 // when writing to the registry. If it's zero, a suitable default will be chosen. 157 // It might be larger if the underlying registry requires that. 158 // 159 // The context remains active as long as the BlobWriter is around: if it's 160 // cancelled, it should cause any blocked BlobWriter operations to terminate. 161 PushBlobChunked(ctx context.Context, repo string, chunkSize int) (BlobWriter, error) 162 163 // PushBlobChunkedResume resumes a previous push of a blob started with PushBlobChunked. 164 // The id should be the value returned from [BlobWriter.ID] from the previous push. 165 // and the offset should be the value returned from [BlobWriter.Size]. 166 // 167 // The offset and chunkSize should similarly be obtained from the previous [BlobWriter] 168 // via the [BlobWriter.Size] and [BlobWriter.ChunkSize] methods. 169 // Alternatively, set offset to -1 to continue where the last write left off, 170 // and to only use chunkSize as a hint like in PushBlobChunked. 171 // 172 // The context remains active as long as the BlobWriter is around: if it's 173 // cancelled, it should cause any blocked BlobWriter operations to terminate. 174 PushBlobChunkedResume(ctx context.Context, repo, id string, offset int64, chunkSize int) (BlobWriter, error) 175 176 // MountBlob makes a blob with the given digest that's in fromRepo available 177 // in toRepo and returns its canonical descriptor. 178 // 179 // This avoids the need to pull content down from fromRepo only to push it to r. 180 // 181 // TODO the mount endpoint doesn't return the size of the content, 182 // so to return a correctly populated descriptor, a client will need to make 183 // an extra HTTP call to find that out. For now, we'll just say that 184 // the descriptor returned from MountBlob might have a zero Size. 185 // 186 // Errors: 187 // ErrUnsupported (when the repository does not support mounts). 188 MountBlob(ctx context.Context, fromRepo, toRepo string, digest Digest) (Descriptor, error) 189 190 // PushManifest pushes a manifest with the given media type and contents. 191 // If tag is non-empty, the tag with that name will be pointed at the manifest. 192 // 193 // It returns a descriptor suitable for accessing the manfiest. 194 PushManifest(ctx context.Context, repo string, tag string, contents []byte, mediaType string) (Descriptor, error) 195 } 196 197 // Deleter defines registry actions that delete objects from the registry. 198 type Deleter interface { 199 // DeleteBlob deletes the blob with the given digest in the given repository. 200 DeleteBlob(ctx context.Context, repo string, digest Digest) error 201 202 // DeleteManifest deletes the manifest with the given digest in the given repository. 203 DeleteManifest(ctx context.Context, repo string, digest Digest) error 204 205 // DeleteTag deletes the manifest with the given tag in the given repository. 206 // TODO does this delete the tag only, or the manifest too? 207 DeleteTag(ctx context.Context, repo string, name string) error 208 } 209 210 // Lister defines registry operations that enumerate objects within the registry. 211 // TODO support resumption from a given point. 212 type Lister interface { 213 // Repositories returns an iterator that can be used to iterate 214 // over all the repositories in the registry in lexical order. 215 // If startAfter is non-empty, the iteration starts lexically 216 // after, but not including, that repository. 217 Repositories(ctx context.Context, startAfter string) Seq[string] 218 219 // Tags returns an iterator that can be used to iterate over all 220 // the tags in the given repository in lexical order. If 221 // startAfter is non-empty, the tags start lexically after, but 222 // not including that tag. 223 Tags(ctx context.Context, repo string, startAfter string) Seq[string] 224 225 // Referrers returns an iterator that can be used to iterate over all 226 // the manifests that have the given digest as their Subject. 227 // If artifactType is non-zero, the results will be restricted to 228 // only manifests with that type. 229 // TODO is it possible to ask for multiple artifact types? 230 Referrers(ctx context.Context, repo string, digest Digest, artifactType string) Seq[Descriptor] 231 } 232 233 // BlobWriter provides a handle for uploading a blob to a registry. 234 type BlobWriter interface { 235 // Write writes more data to the blob. When resuming, the 236 // caller must start writing data from Size bytes into the content. 237 io.Writer 238 239 // Closer closes the writer but does not abort. The blob write 240 // can later be resumed. 241 io.Closer 242 243 // Size returns the number of bytes written to this blob. 244 Size() int64 245 246 // ChunkSize returns the maximum number of bytes to upload at a single time. 247 // This number must meet the minimum given by the registry 248 // and should otherwise follow the hint given by the user. 249 ChunkSize() int 250 251 // ID returns the opaque identifier for this writer. The returned value 252 // can be passed to PushBlobChunked to resume the write. 253 // It is only valid before Write has been called or after Close has 254 // been called. 255 ID() string 256 257 // Commit completes the blob writer process. The content is verified 258 // against the provided digest, and a canonical descriptor for it is returned. 259 Commit(digest Digest) (Descriptor, error) 260 261 // Cancel ends the blob write without storing any data and frees any 262 // associated resources. Any data written thus far will be lost. Cancel 263 // implementations should allow multiple calls even after a commit that 264 // result in a no-op. This allows use of Cancel in a defer statement, 265 // increasing the assurance that it is correctly called. 266 Cancel() error 267 } 268 269 // BlobReader provides the contents of a given blob or manifest. 270 type BlobReader interface { 271 io.ReadCloser 272 // Descriptor returns the descriptor for the blob. 273 Descriptor() Descriptor 274 } 275