cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/lister.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 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 }