ociregistry

package module
v0.0.0-...-eedc705 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2024 License: Apache-2.0 Imports: 12 Imported by: 9

README

ociregistry

In the top level package (ociregistry) this module defines a Go interface that encapsulates the operations provided by an OCI registry.

Full reference documentation can be found here.

It also provides a lightweight in-memory implementation of that interface (ocimem) and an HTTP server that implements the OCI registry protocol on top of it.

The server currently passes the conformance tests.

The aim is to provide an ergonomic interface for defining and layering OCI registry implementations.

Although the API is fairly stable, it's still in v0 currently, so incompatible changes can't be ruled out.

The code was originally derived from the go-containerregistry registry, but has considerably diverged since then.

Documentation

Overview

Package ociregistry provides an abstraction that represents the capabilities provided by an OCI registry.

See the OCI distribution specification for more information on OCI registries.

Packages within this module provide the capability to translate to and from the HTTP protocol documented in that specification: - cuelabs.dev/go/oci/ociregistry/ociclient provides an Interface value that acts as an HTTP client. - cuelabs.dev/go/oci/ociregistry/ociserver provides an HTTP server that serves the distribution protocol by making calls to an arbitrary Interface value.

When used together in a stack, the above two packages can be used to provide a simple proxy server.

The cuelabs.dev/go/oci/ociregistry/ocimem package provides a trivial in-memory implementation of the interface.

Other packages provide some utilities that manipulate Interface values: - cuelabs.dev/go/oci/ociregistry/ocifilter provides functionality for exposing modified or restricted views onto a registry. - cuelabs.dev/go/oci/ociregistry/ociunify can combine two registries into one unified view across both.

Notes on [Interface]

In general, the caller cannot assume that the implementation of a given Interface value is present on the network. For example, cuelabs.dev/go/oci/ociregistry/ocimem doesn't know about the network at all. But there are times when an implementation might want to provide information about the location of blobs or manifests so that a client can go direct if it wishes. That is, a proxy might not wish to ship all the traffic for all blobs through itself, but instead redirect clients to talk to some other location on the internet.

When an Interface implementation wishes to provide that information, it can do so by setting the `URLs` field on the descriptor that it returns for a given blob or manifest. Although it is not mandatory for a caller to use this, some callers (specifically the ociserver package) can use this information to redirect clients appropriately.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrBlobUnknown         = NewError("blob unknown to registry", "BLOB_UNKNOWN", nil)
	ErrBlobUploadInvalid   = NewError("blob upload invalid", "BLOB_UPLOAD_INVALID", nil)
	ErrBlobUploadUnknown   = NewError("blob upload unknown to registry", "BLOB_UPLOAD_UNKNOWN", nil)
	ErrDigestInvalid       = NewError("provided digest did not match uploaded content", "DIGEST_INVALID", nil)
	ErrManifestBlobUnknown = NewError("manifest references a manifest or blob unknown to registry", "MANIFEST_BLOB_UNKNOWN", nil)
	ErrManifestInvalid     = NewError("manifest invalid", "MANIFEST_INVALID", nil)
	ErrManifestUnknown     = NewError("manifest unknown to registry", "MANIFEST_UNKNOWN", nil)
	ErrNameInvalid         = NewError("invalid repository name", "NAME_INVALID", nil)
	ErrNameUnknown         = NewError("repository name not known to registry", "NAME_UNKNOWN", nil)
	ErrSizeInvalid         = NewError("provided length did not match content length", "SIZE_INVALID", nil)
	ErrUnauthorized        = NewError("authentication required", "UNAUTHORIZED", nil)
	ErrDenied              = NewError("requested access to the resource is denied", "DENIED", nil)
	ErrUnsupported         = NewError("the operation is unsupported", "UNSUPPORTED", nil)
	ErrTooManyRequests     = NewError("too many requests", "TOOMANYREQUESTS", nil)

	// ErrRangeInvalid allows Interface implementations to reject invalid ranges,
	// such as a chunked upload PATCH not following the range from a previous PATCH.
	// ociserver relies on this error to return 416 HTTP status codes.
	//
	// It is separate from the Error type since the spec only dictates its HTTP status code,
	// but does not assign any error code to it.
	// We borrowed RANGE_INVALID from the Docker registry implementation, a de facto standard.
	ErrRangeInvalid = NewError("invalid content range", "RANGE_INVALID", nil)
)

The following values represent the known error codes.

Functions

func All

func All[T any](it Seq[T]) (_ []T, _err error)

func IsValidDigest

func IsValidDigest(d string) bool

IsValidDigest reports whether the digest d is well formed.

func IsValidRepoName

func IsValidRepoName(repoName string) bool

IsValidRepoName reports whether the given repository name is valid according to the specification.

func IsValidTag

func IsValidTag(tag string) bool

IsValidTag reports whether the digest d is valid according to the specification.

func MarshalError

func MarshalError(err error) (errorBody []byte, httpStatus int)

MarshalError marshals the given error as JSON according to the OCI distribution specification. It also returns the associated HTTP status code, or http.StatusInternalServerError if no specific code can be found.

If err is or wraps Error, that code will be used for the "code" field in the marshaled error.

If err wraps HTTPError and no HTTP status code is known for the error code, [HTTPError.StatusCode] will be used.

func WriteError

func WriteError(w http.ResponseWriter, err error) error

WriteError marshals the given error as JSON using MarshalError and then writes it to w. It returns the error returned from w.Write.

Types

type BlobReader

type BlobReader interface {
	io.ReadCloser
	// Descriptor returns the descriptor for the blob.
	Descriptor() Descriptor
}

BlobReader provides the contents of a given blob or manifest.

type BlobWriter

type BlobWriter interface {
	// Write writes more data to the blob. When resuming, the
	// caller must start writing data from Size bytes into the content.
	io.Writer

	// Closer closes the writer but does not abort. The blob write
	// can later be resumed.
	io.Closer

	// Size returns the number of bytes written to this blob.
	Size() int64

	// ChunkSize returns the maximum number of bytes to upload at a single time.
	// This number must meet the minimum given by the registry
	// and should otherwise follow the hint given by the user.
	ChunkSize() int

	// ID returns the opaque identifier for this writer. The returned value
	// can be passed to PushBlobChunked to resume the write.
	// It is only valid before Write has been called or after Close has
	// been called.
	ID() string

	// Commit completes the blob writer process. The content is verified
	// against the provided digest, and a canonical descriptor for it is returned.
	Commit(digest Digest) (Descriptor, error)

	// Cancel ends the blob write without storing any data and frees any
	// associated resources. Any data written thus far will be lost. Cancel
	// implementations should allow multiple calls even after a commit that
	// result in a no-op. This allows use of Cancel in a defer statement,
	// increasing the assurance that it is correctly called.
	Cancel() error
}

BlobWriter provides a handle for uploading a blob to a registry.

type Deleter

type Deleter interface {
	// DeleteBlob deletes the blob with the given digest in the given repository.
	DeleteBlob(ctx context.Context, repo string, digest Digest) error

	// DeleteManifest deletes the manifest with the given digest in the given repository.
	DeleteManifest(ctx context.Context, repo string, digest Digest) error

	// DeleteTag deletes the manifest with the given tag in the given repository.
	// TODO does this delete the tag only, or the manifest too?
	DeleteTag(ctx context.Context, repo string, name string) error
}

Deleter defines registry actions that delete objects from the registry.

type Descriptor

type Descriptor = ocispec.Descriptor

type Digest

type Digest = digest.Digest

type Error

type Error interface {
	// error.Error provides the error message.
	error

	// Code returns the error code.
	Code() string

	// Detail returns any detail  associated with the error,
	// or nil if there is none.
	// The caller should not mutate the returned slice.
	Detail() json.RawMessage
}

Error represents an OCI registry error. The set of codes is defined in the distribution specification.

func NewError

func NewError(msg string, code string, detail json.RawMessage) Error

NewError returns a new error with the given code, message and detail.

type Funcs

type Funcs struct {
	NewError func(ctx context.Context, methodName, repo string) error

	GetBlob_               func(ctx context.Context, repo string, digest Digest) (BlobReader, error)
	GetBlobRange_          func(ctx context.Context, repo string, digest Digest, offset0, offset1 int64) (BlobReader, error)
	GetManifest_           func(ctx context.Context, repo string, digest Digest) (BlobReader, error)
	GetTag_                func(ctx context.Context, repo string, tagName string) (BlobReader, error)
	ResolveBlob_           func(ctx context.Context, repo string, digest Digest) (Descriptor, error)
	ResolveManifest_       func(ctx context.Context, repo string, digest Digest) (Descriptor, error)
	ResolveTag_            func(ctx context.Context, repo string, tagName string) (Descriptor, error)
	PushBlob_              func(ctx context.Context, repo string, desc Descriptor, r io.Reader) (Descriptor, error)
	PushBlobChunked_       func(ctx context.Context, repo string, chunkSize int) (BlobWriter, error)
	PushBlobChunkedResume_ func(ctx context.Context, repo, id string, offset int64, chunkSize int) (BlobWriter, error)
	MountBlob_             func(ctx context.Context, fromRepo, toRepo string, digest Digest) (Descriptor, error)
	PushManifest_          func(ctx context.Context, repo string, tag string, contents []byte, mediaType string) (Descriptor, error)
	DeleteBlob_            func(ctx context.Context, repo string, digest Digest) error
	DeleteManifest_        func(ctx context.Context, repo string, digest Digest) error
	DeleteTag_             func(ctx context.Context, repo string, name string) error
	Repositories_          func(ctx context.Context, startAfter string) Seq[string]
	Tags_                  func(ctx context.Context, repo string, startAfter string) Seq[string]
	Referrers_             func(ctx context.Context, repo string, digest Digest, artifactType string) Seq[Descriptor]
}

Funcs implements Interface by calling its member functions: there's one field for every corresponding method of Interface.

When a function is nil, the corresponding method will return an ErrUnsupported error. For nil functions that return an iterator, the corresponding method will return an iterator that returns no items and returns ErrUnsupported from its Err method.

If Funcs is nil itself, all methods will behave as if the corresponding field was nil, so (*ociregistry.Funcs)(nil) is a useful placeholder to implement Interface.

If you're writing your own implementation of Funcs, you'll need to embed a *Funcs value to get an implementation of the private method. This means that it will be possible to add members to Interface in the future without breaking compatibility.

func (*Funcs) DeleteBlob

func (f *Funcs) DeleteBlob(ctx context.Context, repo string, digest Digest) error

func (*Funcs) DeleteManifest

func (f *Funcs) DeleteManifest(ctx context.Context, repo string, digest Digest) error

func (*Funcs) DeleteTag

func (f *Funcs) DeleteTag(ctx context.Context, repo string, name string) error

func (*Funcs) GetBlob

func (f *Funcs) GetBlob(ctx context.Context, repo string, digest Digest) (BlobReader, error)

func (*Funcs) GetBlobRange

func (f *Funcs) GetBlobRange(ctx context.Context, repo string, digest Digest, offset0, offset1 int64) (BlobReader, error)

func (*Funcs) GetManifest

func (f *Funcs) GetManifest(ctx context.Context, repo string, digest Digest) (BlobReader, error)

func (*Funcs) GetTag

func (f *Funcs) GetTag(ctx context.Context, repo string, tagName string) (BlobReader, error)

func (*Funcs) MountBlob

func (f *Funcs) MountBlob(ctx context.Context, fromRepo, toRepo string, digest Digest) (Descriptor, error)

func (*Funcs) PushBlob

func (f *Funcs) PushBlob(ctx context.Context, repo string, desc Descriptor, r io.Reader) (Descriptor, error)

func (*Funcs) PushBlobChunked

func (f *Funcs) PushBlobChunked(ctx context.Context, repo string, chunkSize int) (BlobWriter, error)

func (*Funcs) PushBlobChunkedResume

func (f *Funcs) PushBlobChunkedResume(ctx context.Context, repo, id string, offset int64, chunkSize int) (BlobWriter, error)

func (*Funcs) PushManifest

func (f *Funcs) PushManifest(ctx context.Context, repo string, tag string, contents []byte, mediaType string) (Descriptor, error)

func (*Funcs) Referrers

func (f *Funcs) Referrers(ctx context.Context, repo string, digest Digest, artifactType string) Seq[Descriptor]

func (*Funcs) Repositories

func (f *Funcs) Repositories(ctx context.Context, startAfter string) Seq[string]

func (*Funcs) ResolveBlob

func (f *Funcs) ResolveBlob(ctx context.Context, repo string, digest Digest) (Descriptor, error)

func (*Funcs) ResolveManifest

func (f *Funcs) ResolveManifest(ctx context.Context, repo string, digest Digest) (Descriptor, error)

func (*Funcs) ResolveTag

func (f *Funcs) ResolveTag(ctx context.Context, repo string, tagName string) (Descriptor, error)

func (*Funcs) Tags

func (f *Funcs) Tags(ctx context.Context, repo string, startAfter string) Seq[string]

type HTTPError

type HTTPError interface {
	error

	// StatusCode returns the HTTP status code of the response.
	StatusCode() int

	// Response holds the HTTP response that caused the HTTPError to
	// be created. It will return nil if the error was not created
	// as a result of an HTTP response.
	//
	// The caller should not read the response body or otherwise
	// change the response (mutation of errors is a Bad Thing).
	//
	// Use the ResponseBody method to obtain the body of the
	// response if needed.
	Response() *http.Response

	// ResponseBody returns the contents of the response body. It
	// will return nil if the error was not created as a result of
	// an HTTP response.
	//
	// The caller should not change or append to the returned data.
	ResponseBody() []byte
}

HTTPError is optionally implemented by an error when the error has originated from an HTTP request or might be returned from one.

func NewHTTPError

func NewHTTPError(err error, statusCode int, response *http.Response, body []byte) HTTPError

NewHTTPError returns an error that wraps err to make an HTTPError that represents the given status code, response and response body. Both response and body may be nil.

A shallow copy is made of the response.

type Interface

type Interface interface {
	Writer
	Reader
	Deleter
	Lister
	// contains filtered or unexported methods
}

Interface defines a generic interface to a single OCI registry. It does not support cross-registry operations: all methods are directed to the receiver only.

type Lister

type Lister interface {
	// Repositories returns an iterator that can be used to iterate
	// over all the repositories in the registry in lexical order.
	// If startAfter is non-empty, the iteration starts lexically
	// after, but not including, that repository.
	Repositories(ctx context.Context, startAfter string) Seq[string]

	// Tags returns an iterator that can be used to iterate over all
	// the tags in the given repository in lexical order. If
	// startAfter is non-empty, the tags start lexically after, but
	// not including that tag.
	Tags(ctx context.Context, repo string, startAfter string) Seq[string]

	// Referrers returns an iterator that can be used to iterate over all
	// the manifests that have the given digest as their Subject.
	// If artifactType is non-zero, the results will be restricted to
	// only manifests with that type.
	// TODO is it possible to ask for multiple artifact types?
	Referrers(ctx context.Context, repo string, digest Digest, artifactType string) Seq[Descriptor]
}

Lister defines registry operations that enumerate objects within the registry. TODO support resumption from a given point.

type Manifest

type Manifest = ocispec.Manifest

type ReadWriter

type ReadWriter interface {
	Reader
	Writer
}

type Reader

type Reader interface {
	// GetBlob returns the content of the blob with the given digest.
	// The context also controls the lifetime of the returned BlobReader.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrBlobUnknown when the blob is not present in the repository.
	GetBlob(ctx context.Context, repo string, digest Digest) (BlobReader, error)

	// GetBlobRange is like GetBlob but asks to get only the given range of bytes from the blob,
	// starting at offset0, up to but not including offset1.
	// If offset1 is negative or exceeds the actual size of the blob, GetBlobRange will
	// return all the data starting from offset0.
	// The context also controls the lifetime of the returned BlobReader.
	GetBlobRange(ctx context.Context, repo string, digest Digest, offset0, offset1 int64) (BlobReader, error)

	// GetManifest returns the contents of the manifest with the given digest.
	// The context also controls the lifetime of the returned BlobReader.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrManifestUnknown when the blob is not present in the repository.
	GetManifest(ctx context.Context, repo string, digest Digest) (BlobReader, error)

	// GetTag returns the contents of the manifest with the given tag.
	// The context also controls the lifetime of the returned BlobReader.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrManifestUnknown when the tag is not present in the repository.
	GetTag(ctx context.Context, repo string, tagName string) (BlobReader, error)

	// ResolveDigest returns the descriptor for a given blob.
	// Only the MediaType, Digest and Size fields will be filled out.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrBlobUnknown when the blob is not present in the repository.
	ResolveBlob(ctx context.Context, repo string, digest Digest) (Descriptor, error)

	// ResolveManifest returns the descriptor for a given maniifest.
	// Only the MediaType, Digest and Size fields will be filled out.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrManifestUnknown when the blob is not present in the repository.
	ResolveManifest(ctx context.Context, repo string, digest Digest) (Descriptor, error)

	// ResolveTag returns the descriptor for a given tag.
	// Only the MediaType, Digest and Size fields will be filled out.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrManifestUnknown when the blob is not present in the repository.
	ResolveTag(ctx context.Context, repo string, tagName string) (Descriptor, error)
}

type Seq

type Seq[T any] func(yield func(T, error) bool)

Seq defines the type of an iterator sequence returned from the iterator functions. In general, a non-nil error means that the item is the last in the sequence.

func ErrorSeq

func ErrorSeq[T any](err error) Seq[T]

ErrorSeq returns an iterator that has no items and always returns the given error.

func SliceSeq

func SliceSeq[T any](xs []T) Seq[T]

type WireError

type WireError struct {
	Code_   string `json:"code"`
	Message string `json:"message,omitempty"`
	// Detail_ holds the JSON detail for the message.
	// It's assumed to be valid JSON if non-empty.
	Detail_ json.RawMessage `json:"detail,omitempty"`
}

WireError holds a single error in an OCI HTTP response.

func (*WireError) Code

func (e *WireError) Code() string

Code implements [Error.Code].

func (*WireError) Detail

func (e *WireError) Detail() json.RawMessage

Detail implements [Error.Detail].

func (*WireError) Error

func (e *WireError) Error() string

Error implements the [error] interface.

func (*WireError) Is

func (e *WireError) Is(err error) bool

Is makes it possible for users to write `if errors.Is(err, ociregistry.ErrBlobUnknown)` even when the error hasn't exactly wrapped that error.

type WireErrors

type WireErrors struct {
	Errors []WireError `json:"errors"`
}

WireErrors is the JSON format used for error responses in the OCI HTTP API. It should always contain at least one error.

func (*WireErrors) Error

func (e *WireErrors) Error() string

func (*WireErrors) Unwrap

func (e *WireErrors) Unwrap() []error

Unwrap allows errors.Is and errors.As to see the errors inside e.

type Writer

type Writer interface {
	// PushBlob pushes a blob described by desc to the given repository, reading content from r.
	// Only the desc.Digest and desc.Size fields are used.
	// It returns desc with Digest set to the canonical digest for the blob.
	// Errors:
	// - ErrNameUnknown when the repository is not present.
	// - ErrNameInvalid when the repository name is not valid.
	// - ErrDigestInvalid when desc.Digest does not match the content.
	// - ErrSizeInvalid when desc.Size does not match the content length.
	PushBlob(ctx context.Context, repo string, desc Descriptor, r io.Reader) (Descriptor, error)

	// PushBlobChunked starts to push a blob to the given repository.
	// The returned [BlobWriter] can be used to stream the upload and resume on temporary errors.
	//
	// The chunkSize parameter provides a hint for the chunk size to use
	// when writing to the registry. If it's zero, a suitable default will be chosen.
	// It might be larger if the underlying registry requires that.
	//
	// The context remains active as long as the BlobWriter is around: if it's
	// cancelled, it should cause any blocked BlobWriter operations to terminate.
	PushBlobChunked(ctx context.Context, repo string, chunkSize int) (BlobWriter, error)

	// PushBlobChunkedResume resumes a previous push of a blob started with PushBlobChunked.
	// The id should be the value returned from [BlobWriter.ID] from the previous push.
	// and the offset should be the value returned from [BlobWriter.Size].
	//
	// The offset and chunkSize should similarly be obtained from the previous [BlobWriter]
	// via the [BlobWriter.Size] and [BlobWriter.ChunkSize] methods.
	// Alternatively, set offset to -1 to continue where the last write left off,
	// and to only use chunkSize as a hint like in PushBlobChunked.
	//
	// The context remains active as long as the BlobWriter is around: if it's
	// cancelled, it should cause any blocked BlobWriter operations to terminate.
	PushBlobChunkedResume(ctx context.Context, repo, id string, offset int64, chunkSize int) (BlobWriter, error)

	// MountBlob makes a blob with the given digest that's in fromRepo available
	// in toRepo and returns its canonical descriptor.
	//
	// This avoids the need to pull content down from fromRepo only to push it to r.
	//
	// TODO the mount endpoint doesn't return the size of the content,
	// so to return a correctly populated descriptor, a client will need to make
	// an extra HTTP call to find that out. For now, we'll just say that
	// the descriptor returned from MountBlob might have a zero Size.
	//
	// Errors:
	//	ErrUnsupported (when the repository does not support mounts).
	MountBlob(ctx context.Context, fromRepo, toRepo string, digest Digest) (Descriptor, error)

	// PushManifest pushes a manifest with the given media type and contents.
	// If tag is non-empty, the tag with that name will be pointed at the manifest.
	//
	// It returns a descriptor suitable for accessing the manfiest.
	PushManifest(ctx context.Context, repo string, tag string, contents []byte, mediaType string) (Descriptor, error)
}

Writer defines registry actions that write to blobs, manifests and tags.

Directories

Path Synopsis
internal
Package ociclient provides an implementation of ociregistry.Interface that uses HTTP to talk to the remote registry.
Package ociclient provides an implementation of ociregistry.Interface that uses HTTP to talk to the remote registry.
Package ocidebug is an OCI registry wrapper that prints log messages on registry operations.
Package ocidebug is an OCI registry wrapper that prints log messages on registry operations.
Package ocifilter implements "filter" functions that wrap or combine ociregistry implementations in different ways.
Package ocifilter implements "filter" functions that wrap or combine ociregistry implementations in different ways.
Package ocimem provides a simple in-memory implementation of an OCI registry.
Package ocimem provides a simple in-memory implementation of an OCI registry.
Package ociref supports parsing cross-registry OCI registry references.
Package ociref supports parsing cross-registry OCI registry references.
Package ociserver implements a docker V2 registry and the OCI distribution specification.
Package ociserver implements a docker V2 registry and the OCI distribution specification.
Package ocitest provides some helper types for writing ociregistry-related tests.
Package ocitest provides some helper types for writing ociregistry-related tests.
Package ociunify unifies two OCI registries into one.
Package ociunify unifies two OCI registries into one.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL