cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/registry.go (about)

     1  // Copyright 2018 Google LLC All Rights Reserved.
     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 ociserver implements a docker V2 registry and the OCI distribution specification.
    16  //
    17  // It is designed to be used anywhere a low dependency container registry is needed.
    18  //
    19  // Its goal is to be standards compliant and its strictness will increase over time.
    20  package ociserver
    21  
    22  import (
    23  	"context"
    24  	"errors"
    25  	"fmt"
    26  	"log"
    27  	"net/http"
    28  	"sync/atomic"
    29  
    30  	"cuelabs.dev/go/oci/ociregistry"
    31  	"cuelabs.dev/go/oci/ociregistry/internal/ocirequest"
    32  	ocispecroot "github.com/opencontainers/image-spec/specs-go"
    33  )
    34  
    35  // debug causes debug messages to be emitted when running the server.
    36  const debug = false
    37  
    38  var v2 = ocispecroot.Versioned{
    39  	SchemaVersion: 2,
    40  }
    41  
    42  // Options holds options for the server.
    43  type Options struct {
    44  	// WriteError is used to write error responses. It is passed the
    45  	// writer to write the error response to, the request that
    46  	// the error is in response to, and the error itself.
    47  	//
    48  	// If WriteError is nil, [ociregistry.WriteError] will
    49  	// be used and any error discarded.
    50  	WriteError func(w http.ResponseWriter, req *http.Request, err error)
    51  
    52  	// DisableReferrersAPI, when true, causes the registry to behave as if
    53  	// it does not understand the referrers API.
    54  	DisableReferrersAPI bool
    55  
    56  	// DisableSinglePostUpload, when true, causes the registry
    57  	// to reject uploads with a single POST request.
    58  	// This is useful in combination with LocationsForDescriptor
    59  	// to cause uploaded blob content to flow through
    60  	// another server.
    61  	DisableSinglePostUpload bool
    62  
    63  	// MaxListPageSize, if > 0, causes the list endpoints to return an
    64  	// error if the page size is greater than that. This emulates
    65  	// a quirk of AWS ECR where it refuses request for any
    66  	// page size > 1000.
    67  	MaxListPageSize int
    68  
    69  	// OmitDigestFromTagGetResponse causes the registry
    70  	// to omit the Docker-Content-Digest header from a tag
    71  	// GET response, mimicking the behavior of registries that
    72  	// do the same (for example AWS ECR).
    73  	OmitDigestFromTagGetResponse bool
    74  
    75  	// OmitLinkHeaderFromResponses causes the server
    76  	// to leave out the Link header from list responses.
    77  	OmitLinkHeaderFromResponses bool
    78  
    79  	// LocationForUploadID transforms an upload ID as returned by
    80  	// ocirequest.BlobWriter.ID to the absolute URL location
    81  	// as returned by the upload endpoints.
    82  	//
    83  	// By default, when this function is nil, or it returns an empty
    84  	// string, upload IDs are treated as opaque identifiers and the
    85  	// returned locations are always host-relative URLs into the
    86  	// server itself.
    87  	//
    88  	// This can be used to allow clients to fetch and push content
    89  	// directly from some upstream server rather than passing
    90  	// through this server. Clients doing that will need access
    91  	// rights to that remote location.
    92  	LocationForUploadID func(string) (string, error)
    93  
    94  	// LocationsForDescriptor returns a set of possible download
    95  	// URLs for the given descriptor.
    96  	// If it's nil, then all locations returned by the server
    97  	// will refer to the server itself.
    98  	//
    99  	// If not, then the Location header of responses will be
   100  	// set accordingly (to an arbitrary value from the
   101  	// returned slice if there are multiple).
   102  	//
   103  	// Returning a location from this function will also
   104  	// cause GET requests to return a redirect response
   105  	// to that location.
   106  	//
   107  	// TODO perhaps the redirect behavior described above
   108  	// isn't always what is wanted?
   109  	LocationsForDescriptor func(isManifest bool, desc ociregistry.Descriptor) ([]string, error)
   110  
   111  	DebugID string
   112  }
   113  
   114  var debugID int32
   115  
   116  // New returns a handler which implements the docker registry protocol
   117  // by making calls to the underlying registry backend r.
   118  //
   119  // If opts is nil, it's equivalent to passing new(Options).
   120  //
   121  // The returned handler should be registered at the site root.
   122  //
   123  // # Errors
   124  //
   125  // All HTTP responses will be JSON, formatted according to the
   126  // OCI spec. If an error returned from backend conforms to
   127  // [ociregistry.Error], the associated code and detail will be used.
   128  //
   129  // The HTTP response code will be determined from the error
   130  // code when possible. If it can't be determined and the
   131  // error implements [ociregistry.HTTPError], the code returned
   132  // by StatusCode will be used as the HTTP response code.
   133  func New(backend ociregistry.Interface, opts *Options) http.Handler {
   134  	if opts == nil {
   135  		opts = new(Options)
   136  	}
   137  	r := &registry{
   138  		opts:    *opts,
   139  		backend: backend,
   140  	}
   141  	if r.opts.DebugID == "" {
   142  		r.opts.DebugID = fmt.Sprintf("ociserver%d", atomic.AddInt32(&debugID, 1))
   143  	}
   144  	if r.opts.WriteError == nil {
   145  		r.opts.WriteError = func(w http.ResponseWriter, _ *http.Request, err error) {
   146  			ociregistry.WriteError(w, err)
   147  		}
   148  	}
   149  	return r
   150  }
   151  
   152  func (r *registry) logf(f string, a ...any) {
   153  	log.Printf("ociserver %s: %s", r.opts.DebugID, fmt.Sprintf(f, a...))
   154  }
   155  
   156  type registry struct {
   157  	opts    Options
   158  	backend ociregistry.Interface
   159  }
   160  
   161  var handlers = []func(r *registry, ctx context.Context, w http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error{
   162  	ocirequest.ReqPing:               (*registry).handlePing,
   163  	ocirequest.ReqBlobGet:            (*registry).handleBlobGet,
   164  	ocirequest.ReqBlobHead:           (*registry).handleBlobHead,
   165  	ocirequest.ReqBlobDelete:         (*registry).handleBlobDelete,
   166  	ocirequest.ReqBlobStartUpload:    (*registry).handleBlobStartUpload,
   167  	ocirequest.ReqBlobUploadBlob:     (*registry).handleBlobUploadBlob,
   168  	ocirequest.ReqBlobMount:          (*registry).handleBlobMount,
   169  	ocirequest.ReqBlobUploadInfo:     (*registry).handleBlobUploadInfo,
   170  	ocirequest.ReqBlobUploadChunk:    (*registry).handleBlobUploadChunk,
   171  	ocirequest.ReqBlobCompleteUpload: (*registry).handleBlobCompleteUpload,
   172  	ocirequest.ReqManifestGet:        (*registry).handleManifestGet,
   173  	ocirequest.ReqManifestHead:       (*registry).handleManifestHead,
   174  	ocirequest.ReqManifestPut:        (*registry).handleManifestPut,
   175  	ocirequest.ReqManifestDelete:     (*registry).handleManifestDelete,
   176  	ocirequest.ReqTagsList:           (*registry).handleTagsList,
   177  	ocirequest.ReqReferrersList:      (*registry).handleReferrersList,
   178  	ocirequest.ReqCatalogList:        (*registry).handleCatalogList,
   179  }
   180  
   181  func (r *registry) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
   182  	if rerr := r.v2(resp, req); rerr != nil {
   183  		r.opts.WriteError(resp, req, rerr)
   184  		return
   185  	}
   186  }
   187  
   188  // https://docs.docker.com/registry/spec/api/#api-version-check
   189  // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check
   190  func (r *registry) v2(resp http.ResponseWriter, req *http.Request) (_err error) {
   191  	if debug {
   192  		r.logf("registry.v2 %v %s {", req.Method, req.URL)
   193  		defer func() {
   194  			if _err != nil {
   195  				r.logf("} -> %v", _err)
   196  			} else {
   197  				r.logf("}")
   198  			}
   199  		}()
   200  	}
   201  
   202  	rreq, err := ocirequest.Parse(req.Method, req.URL)
   203  	if err != nil {
   204  		resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
   205  		return handlerErrorForRequestParseError(err)
   206  	}
   207  	handle := handlers[rreq.Kind]
   208  	return handle(r, req.Context(), resp, req, rreq)
   209  }
   210  
   211  func (r *registry) handlePing(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
   212  	resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
   213  	return nil
   214  }
   215  
   216  func (r *registry) setLocationHeader(resp http.ResponseWriter, isManifest bool, desc ociregistry.Descriptor, defaultLocation string) error {
   217  	loc := defaultLocation
   218  	if r.opts.LocationsForDescriptor != nil {
   219  		locs, err := r.opts.LocationsForDescriptor(isManifest, desc)
   220  		if err != nil {
   221  			what := "blob"
   222  			if isManifest {
   223  				what = "manifest"
   224  			}
   225  			return fmt.Errorf("cannot determine location for %s: %v", what, err)
   226  		}
   227  		if len(locs) > 0 {
   228  			loc = locs[0] // TODO select arbitrary location from the slice
   229  		}
   230  	}
   231  	resp.Header().Set("Location", loc)
   232  	resp.Header().Set("Docker-Content-Digest", string(desc.Digest))
   233  	return nil
   234  }
   235  
   236  // ParseError represents an error that can happen when parsing.
   237  // The Err field holds one of the possible error values below.
   238  type ParseError struct {
   239  	error
   240  }
   241  
   242  func handlerErrorForRequestParseError(err error) error {
   243  	if err == nil {
   244  		return nil
   245  	}
   246  	var perr *ocirequest.ParseError
   247  	if !errors.As(err, &perr) {
   248  		return err
   249  	}
   250  	switch perr.Err {
   251  	case ocirequest.ErrNotFound:
   252  		return withHTTPCode(http.StatusNotFound, err)
   253  	case ocirequest.ErrBadlyFormedDigest:
   254  		return withHTTPCode(http.StatusBadRequest, err)
   255  	case ocirequest.ErrMethodNotAllowed:
   256  		return withHTTPCode(http.StatusMethodNotAllowed, err)
   257  	case ocirequest.ErrBadRequest:
   258  		return withHTTPCode(http.StatusBadRequest, err)
   259  	}
   260  	return err
   261  }