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 := ®istry{ 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 }