cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociclient/lister.go (about) 1 // Copyright 2023 CUE Labs AG 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 ociclient 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "io" 22 "net/http" 23 "strings" 24 25 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 26 27 "cuelabs.dev/go/oci/ociregistry" 28 "cuelabs.dev/go/oci/ociregistry/internal/ocirequest" 29 ) 30 31 func (c *client) Repositories(ctx context.Context, startAfter string) ociregistry.Seq[string] { 32 return c.pager(ctx, &ocirequest.Request{ 33 Kind: ocirequest.ReqCatalogList, 34 ListN: c.listPageSize, 35 ListLast: startAfter, 36 }, func(resp *http.Response) ([]string, error) { 37 data, err := io.ReadAll(resp.Body) 38 if err != nil { 39 return nil, err 40 } 41 var catalog struct { 42 Repos []string `json:"repositories"` 43 } 44 if err := json.Unmarshal(data, &catalog); err != nil { 45 return nil, fmt.Errorf("cannot unmarshal catalog response: %v", err) 46 } 47 return catalog.Repos, nil 48 }) 49 } 50 51 func (c *client) Tags(ctx context.Context, repoName, startAfter string) ociregistry.Seq[string] { 52 return c.pager(ctx, &ocirequest.Request{ 53 Kind: ocirequest.ReqTagsList, 54 Repo: repoName, 55 ListN: c.listPageSize, 56 ListLast: startAfter, 57 }, func(resp *http.Response) ([]string, error) { 58 data, err := io.ReadAll(resp.Body) 59 if err != nil { 60 return nil, err 61 } 62 var tagsResponse struct { 63 Repo string `json:"name"` 64 Tags []string `json:"tags"` 65 } 66 if err := json.Unmarshal(data, &tagsResponse); err != nil { 67 return nil, fmt.Errorf("cannot unmarshal tags list response: %v", err) 68 } 69 return tagsResponse.Tags, nil 70 }) 71 } 72 73 func (c *client) Referrers(ctx context.Context, repoName string, digest ociregistry.Digest, artifactType string) ociregistry.Seq[ociregistry.Descriptor] { 74 // TODO paging 75 resp, err := c.doRequest(ctx, &ocirequest.Request{ 76 Kind: ocirequest.ReqReferrersList, 77 Repo: repoName, 78 Digest: string(digest), 79 ListN: c.listPageSize, 80 }) 81 if err != nil { 82 return ociregistry.ErrorSeq[ociregistry.Descriptor](err) 83 } 84 85 data, err := io.ReadAll(resp.Body) 86 resp.Body.Close() 87 if err != nil { 88 return ociregistry.ErrorSeq[ociregistry.Descriptor](err) 89 } 90 var referrersResponse ocispec.Index 91 if err := json.Unmarshal(data, &referrersResponse); err != nil { 92 return ociregistry.ErrorSeq[ociregistry.Descriptor](fmt.Errorf("cannot unmarshal referrers response: %v", err)) 93 } 94 return ociregistry.SliceSeq(referrersResponse.Manifests) 95 } 96 97 // pager returns an iterator for a list entry point. It starts by sending the given 98 // initial request and parses each response into its component items using 99 // parseResponse. It tries to use the Link header in each response to continue 100 // the iteration, falling back to using the "last" query parameter. 101 func (c *client) pager(ctx context.Context, initialReq *ocirequest.Request, parseResponse func(*http.Response) ([]string, error)) ociregistry.Seq[string] { 102 return func(yield func(string, error) bool) { 103 // We assume that the same scope is applicable to all page requests. 104 req, err := newRequest(ctx, initialReq, nil) 105 if err != nil { 106 yield("", err) 107 return 108 } 109 for { 110 resp, err := c.do(req) 111 if err != nil { 112 yield("", err) 113 return 114 } 115 items, err := parseResponse(resp) 116 resp.Body.Close() 117 if err != nil { 118 yield("", err) 119 return 120 } 121 // TODO sanity check that items are in lexical order? 122 for _, item := range items { 123 if !yield(item, nil) { 124 return 125 } 126 } 127 if len(items) < initialReq.ListN { 128 // From the distribution spec: 129 // The response to such a request MAY return fewer than <int> results, 130 // but only when the total number of tags attached to the repository 131 // is less than <int>. 132 return 133 } 134 req, err = nextLink(ctx, resp, initialReq, items[len(items)-1]) 135 if err != nil { 136 yield("", fmt.Errorf("invalid Link header in response: %v", err)) 137 return 138 } 139 } 140 } 141 } 142 143 // nextLink tries to form a request that can be sent to obtain the next page 144 // in a set of list results. 145 // The given response holds the response received from the previous 146 // list request; initialReq holds the request that initiated the listing, 147 // and last holds the final item returned in the previous response. 148 func nextLink(ctx context.Context, resp *http.Response, initialReq *ocirequest.Request, last string) (*http.Request, error) { 149 link0 := resp.Header.Get("Link") 150 if link0 == "" { 151 // This is beyond the first page and there was no Link 152 // in the previous response (the standard doesn't mandate 153 // one), so add a "last" parameter to the initial request. 154 rreq := *initialReq 155 rreq.ListLast = last 156 req, err := newRequest(ctx, &rreq, nil) 157 if err != nil { 158 // Given that we could form the initial request, this should 159 // never happen. 160 return nil, fmt.Errorf("cannot form next request: %v", err) 161 } 162 return req, nil 163 } 164 // Parse the link header according to RFC 5988. 165 // TODO perhaps we shouldn't ignore the relation type? 166 link, ok := strings.CutPrefix(link0, "<") 167 if !ok { 168 return nil, fmt.Errorf("no initial < character in Link=%q", link0) 169 } 170 link, _, ok = strings.Cut(link, ">") 171 if !ok { 172 return nil, fmt.Errorf("no > character in Link=%q", link0) 173 } 174 // Parse it with respect to the originating request, as it's probably relative. 175 linkURL, err := resp.Request.URL.Parse(link) 176 if err != nil { 177 return nil, fmt.Errorf("invalid URL in Link=%q", link0) 178 } 179 return http.NewRequestWithContext(ctx, "GET", linkURL.String(), nil) 180 }