...

Source file src/cuelabs.dev/go/oci/ociregistry/ociserver/registry.go

Documentation: cuelabs.dev/go/oci/ociregistry/ociserver

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

View as plain text