...

Source file src/cuelabs.dev/go/oci/ociregistry/ociclient/lister.go

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

     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.ErrorIter[ociregistry.Descriptor](err)
    83  	}
    84  
    85  	data, err := io.ReadAll(resp.Body)
    86  	resp.Body.Close()
    87  	if err != nil {
    88  		return ociregistry.ErrorIter[ociregistry.Descriptor](err)
    89  	}
    90  	var referrersResponse ocispec.Index
    91  	if err := json.Unmarshal(data, &referrersResponse); err != nil {
    92  		return ociregistry.ErrorIter[ociregistry.Descriptor](fmt.Errorf("cannot unmarshal referrers response: %v", err))
    93  	}
    94  	return ociregistry.SliceIter(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  }
   181  

View as plain text