...

Source file src/cuelabs.dev/go/oci/ociregistry/ociserver/lister.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
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"strconv"
    26  
    27  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    28  
    29  	"cuelabs.dev/go/oci/ociregistry"
    30  	"cuelabs.dev/go/oci/ociregistry/internal/ocirequest"
    31  )
    32  
    33  const maxPageSize = 10000
    34  
    35  type catalog struct {
    36  	Repos []string `json:"repositories"`
    37  }
    38  
    39  type listTags struct {
    40  	Name string   `json:"name"`
    41  	Tags []string `json:"tags"`
    42  }
    43  
    44  func (r *registry) handleTagsList(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) error {
    45  	tags, link, err := r.nextListResults(req, rreq, r.backend.Tags(ctx, rreq.Repo, rreq.ListLast))
    46  	if err != nil {
    47  		return err
    48  	}
    49  	msg, _ := json.Marshal(listTags{
    50  		Name: rreq.Repo,
    51  		Tags: tags,
    52  	})
    53  	if link != "" {
    54  		resp.Header().Set("Link", link)
    55  	}
    56  	resp.Header().Set("Content-Length", strconv.Itoa(len(msg)))
    57  	resp.WriteHeader(http.StatusOK)
    58  	resp.Write(msg)
    59  	return nil
    60  }
    61  
    62  func (r *registry) handleCatalogList(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) (_err error) {
    63  	repos, link, err := r.nextListResults(req, rreq, r.backend.Repositories(ctx, rreq.ListLast))
    64  	if err != nil {
    65  		return err
    66  	}
    67  	msg, err := json.Marshal(catalog{
    68  		Repos: repos,
    69  	})
    70  	if err != nil {
    71  		return err
    72  	}
    73  	if link != "" {
    74  		resp.Header().Set("Link", link)
    75  	}
    76  	resp.Header().Set("Content-Length", strconv.Itoa(len(msg)))
    77  	resp.WriteHeader(http.StatusOK)
    78  	io.Copy(resp, bytes.NewReader([]byte(msg)))
    79  	return nil
    80  }
    81  
    82  // TODO: implement handling of artifactType querystring
    83  func (r *registry) handleReferrersList(ctx context.Context, resp http.ResponseWriter, req *http.Request, rreq *ocirequest.Request) (_err error) {
    84  	if r.opts.DisableReferrersAPI {
    85  		return withHTTPCode(http.StatusNotFound, fmt.Errorf("referrers API has been disabled"))
    86  	}
    87  
    88  	im := &ocispec.Index{
    89  		Versioned: v2,
    90  		MediaType: mediaTypeOCIImageIndex,
    91  	}
    92  
    93  	// TODO support artifactType filtering
    94  	it := r.backend.Referrers(ctx, rreq.Repo, ociregistry.Digest(rreq.Digest), "")
    95  	// TODO(go1.23) for desc, err := range it {
    96  	it(func(desc ociregistry.Descriptor, err error) bool {
    97  		if err != nil {
    98  			_err = err
    99  			return false
   100  		}
   101  		im.Manifests = append(im.Manifests, desc)
   102  		return true
   103  	})
   104  	if _err != nil {
   105  		return _err
   106  	}
   107  	msg, err := json.Marshal(im)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	resp.Header().Set("Content-Length", strconv.Itoa(len(msg)))
   112  	resp.Header().Set("Content-Type", "application/vnd.oci.image.index.v1+json")
   113  	resp.WriteHeader(http.StatusOK)
   114  	resp.Write(msg)
   115  	return nil
   116  }
   117  
   118  func (r *registry) nextListResults(req *http.Request, rreq *ocirequest.Request, itemsIter ociregistry.Seq[string]) (items []string, link string, _err error) {
   119  	if r.opts.MaxListPageSize > 0 && rreq.ListN > r.opts.MaxListPageSize {
   120  		return nil, "", ociregistry.NewError(fmt.Sprintf("query parameter n is too large (n=%d, max=%d)", rreq.ListN, r.opts.MaxListPageSize), ociregistry.ErrUnsupported.Code(), nil)
   121  	}
   122  	n := rreq.ListN
   123  	if n <= 0 {
   124  		n = maxPageSize
   125  	}
   126  	truncated := false
   127  	// TODO(go1.23) for repo, err := range itemsIter {
   128  	itemsIter(func(item string, err error) bool {
   129  		if err != nil {
   130  			_err = err
   131  			return false
   132  		}
   133  		if rreq.ListN > 0 && len(items) >= rreq.ListN {
   134  			truncated = true
   135  			return false
   136  		}
   137  		// TODO we might want some way to limit on the total number
   138  		// of items returned in the absence of a ListN limit.
   139  		items = append(items, item)
   140  		// TODO sanity check that the items are in lexical order?
   141  		return true
   142  	})
   143  	if _err != nil {
   144  		return nil, "", _err
   145  	}
   146  	if truncated && !r.opts.OmitLinkHeaderFromResponses {
   147  		link = r.makeNextLink(req, items[len(items)-1])
   148  	}
   149  	return items, link, nil
   150  }
   151  
   152  // makeNextLink returns an RFC 5988 Link value suitable for
   153  // providing the next URL in a chain of list page results,
   154  // starting after the given "startAfter" item.
   155  // TODO this assumes that req.URL.Path is the actual
   156  // path that the client used to access the server. This might
   157  // not necessarily be true, so maybe it would be better to
   158  // use a path-relative URL instead, although that's trickier
   159  // to arrange.
   160  func (r *registry) makeNextLink(req *http.Request, startAfter string) string {
   161  	// Use the "next" relation type:
   162  	// See https://html.spec.whatwg.org/multipage/links.html#link-type-next
   163  	query := req.URL.Query()
   164  	query.Set("last", startAfter)
   165  	u := &url.URL{
   166  		Path:     req.URL.Path,
   167  		RawQuery: query.Encode(),
   168  	}
   169  	return fmt.Sprintf(`<%v>;rel="next"`, u)
   170  }
   171  

View as plain text