@@ -17,9 +17,11 @@ package ociclient
1717import (
1818 "context"
1919 "encoding/json"
20+ "errors"
2021 "fmt"
2122 "io"
2223 "net/http"
24+ "slices"
2325 "strings"
2426
2527 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -29,11 +31,11 @@ import (
2931)
3032
3133func (c * client ) Repositories (ctx context.Context , startAfter string ) ociregistry.Seq [string ] {
32- return c . pager (ctx , & ocirequest.Request {
34+ return pager (ctx , c , & ocirequest.Request {
3335 Kind : ocirequest .ReqCatalogList ,
3436 ListN : c .listPageSize ,
3537 ListLast : startAfter ,
36- }, func (resp * http.Response ) ([]string , error ) {
38+ }, true , func (resp * http.Response ) ([]string , error ) {
3739 data , err := io .ReadAll (resp .Body )
3840 if err != nil {
3941 return nil , err
@@ -49,12 +51,12 @@ func (c *client) Repositories(ctx context.Context, startAfter string) ociregistr
4951}
5052
5153func (c * client ) Tags (ctx context.Context , repoName , startAfter string ) ociregistry.Seq [string ] {
52- return c . pager (ctx , & ocirequest.Request {
54+ return pager (ctx , c , & ocirequest.Request {
5355 Kind : ocirequest .ReqTagsList ,
5456 Repo : repoName ,
5557 ListN : c .listPageSize ,
5658 ListLast : startAfter ,
57- }, func (resp * http.Response ) ([]string , error ) {
59+ }, true , func (resp * http.Response ) ([]string , error ) {
5860 data , err := io .ReadAll (resp .Body )
5961 if err != nil {
6062 return nil , err
@@ -71,54 +73,80 @@ func (c *client) Tags(ctx context.Context, repoName, startAfter string) ociregis
7173}
7274
7375func (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 )
76+ return pager (ctx , c , & ocirequest.Request {
77+ Kind : ocirequest .ReqReferrersList ,
78+ Repo : repoName ,
79+ Digest : string (digest ),
80+ ListN : c .listPageSize ,
81+ ArtifactType : artifactType ,
82+ }, false , func (resp * http.Response ) ([]ociregistry.Descriptor , error ) {
83+ body := resp .Body
84+ if resp .StatusCode == http .StatusNotFound {
85+ body .Close ()
86+ body = nil
87+ // Fall back to the referrers tag API.
88+ // From https://github.com/opencontainers/distribution-spec/blob/main/spec.md#unavailable-referrers-api :
89+ // A client querying the referrers API and receiving a
90+ // 404 Not Found MUST fallback to using an image index
91+ // pushed to a tag described by the referrers tag
92+ // schema.
93+ r , err := c .GetTag (ctx , repoName , referrersTag (digest ))
94+ if err != nil {
95+ if errors .Is (err , ociregistry .ErrManifestUnknown ) {
96+ return nil , nil
97+ }
98+ return nil , err
99+ }
100+ body = r
101+ }
102+ data , err := io .ReadAll (body )
103+ body .Close ()
104+ if err != nil {
105+ return nil , err
106+ }
107+ var referrersResponse ocispec.Index
108+ if err := json .Unmarshal (data , & referrersResponse ); err != nil {
109+ return nil , fmt .Errorf ("cannot unmarshal referrers response: %v" , err )
110+ }
111+ if artifactType == "" || resp .Header .Get ("OCI-Filters-Applied" ) == "artifactType" {
112+ return referrersResponse .Manifests , nil
113+ }
114+ // The server hasn't filtered the responses, so we must.
115+ // TODO is it OK to assume that the index contains correctly populated
116+ // artifact type and attributes fields when we've fallen back to the referrer tags API?
117+ // If not, we might have to retrieve all the individual manifests to check that info.
118+ manifests := slices .DeleteFunc (referrersResponse .Manifests , func (desc ociregistry.Descriptor ) bool {
119+ return desc .ArtifactType != artifactType
120+ })
121+ return manifests , nil
122+ }, http .StatusOK , http .StatusNotFound )
95123}
96124
97125// pager returns an iterator for a list entry point. It starts by sending the given
98126// initial request and parses each response into its component items using
99127// 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.
128+ // the iteration, falling back to using the "last" query parameter if
129+ // canUseLast is true.
130+ func pager [T any ](ctx context.Context , c * client , initialReq * ocirequest.Request , canUseLast bool , parseResponse func (* http.Response ) ([]T , error ), okStatuses ... int ) ociregistry.Seq [T ] {
131+ return func (yield func (T , error ) bool ) {
132+ // We assume that the same auth scope is applicable to all page requests.
104133 req , err := newRequest (ctx , initialReq , nil )
105134 if err != nil {
106- yield ("" , err )
135+ yield (* new ( T ) , err )
107136 return
108137 }
109138 for {
110- resp , err := c .do (req )
139+ resp , err := c .do (req , okStatuses ... )
111140 if err != nil {
112- yield ("" , err )
141+ yield (* new ( T ) , err )
113142 return
114143 }
115144 items , err := parseResponse (resp )
116145 resp .Body .Close ()
117146 if err != nil {
118- yield ("" , err )
147+ yield (* new ( T ) , err )
119148 return
120149 }
121- // TODO sanity check that items are in lexical order?
122150 for _ , item := range items {
123151 if ! yield (item , nil ) {
124152 return
@@ -131,28 +159,35 @@ func (c *client) pager(ctx context.Context, initialReq *ocirequest.Request, pars
131159 // is less than <int>.
132160 return
133161 }
134- req , err = nextLink (ctx , resp , initialReq , items [len (items )- 1 ])
162+ req , err = nextLink (ctx , resp , initialReq , canUseLast , items [len (items )- 1 ])
135163 if err != nil {
136- yield ("" , fmt .Errorf ("invalid Link header in response: %v" , err ))
164+ yield (* new (T ), fmt .Errorf ("invalid Link header in response: %v" , err ))
165+ return
166+ }
167+ if req == nil {
168+ // No link found; assume there are no more items.
137169 return
138170 }
139171 }
140172 }
141173}
142174
143- // nextLink tries to form a request that can be sent to obtain the next page
175+ // nextLink ttries to form a request that can be sent to obtain the next page
144176// in a set of list results.
145177// The given response holds the response received from the previous
146178// list request; initialReq holds the request that initiated the listing,
147179// 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 ) {
180+ func nextLink [ T any ] (ctx context.Context , resp * http.Response , initialReq * ocirequest.Request , canUseLast bool , last T ) (* http.Request , error ) {
149181 link0 := resp .Header .Get ("Link" )
150182 if link0 == "" {
183+ if ! canUseLast {
184+ return nil , nil
185+ }
151186 // This is beyond the first page and there was no Link
152187 // in the previous response (the standard doesn't mandate
153188 // one), so add a "last" parameter to the initial request.
154189 rreq := * initialReq
155- rreq .ListLast = last
190+ rreq .ListLast = fmt . Sprint ( last )
156191 req , err := newRequest (ctx , & rreq , nil )
157192 if err != nil {
158193 // Given that we could form the initial request, this should
@@ -178,3 +213,38 @@ func nextLink(ctx context.Context, resp *http.Response, initialReq *ocirequest.R
178213 }
179214 return http .NewRequestWithContext (ctx , "GET" , linkURL .String (), nil )
180215}
216+
217+ // referrersTag returns the referrers tag for the given digest, as described
218+ // in https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
219+ func referrersTag (digest ociregistry.Digest ) string {
220+ // It's hard to know what the spec means by "with any characters not allowed by <reference> tags replaced with -",
221+ // because different characters are allowed in different contexts (for example, a dot character
222+ // is allowed except when it's at the start.
223+ // In practice, however, the set of characters is very limited, and the only
224+ // disallowed character in common use is :, so just use a naive algorithm.
225+ return truncateAndMap (digest .Algorithm ().String (), 32 ) + "-" + truncateAndMap (digest .Encoded (), 64 )
226+ }
227+
228+ func truncateAndMap (s string , n int ) string {
229+ // regexp: [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
230+
231+ s = strings .Map (func (r rune ) rune {
232+ switch {
233+ case 'a' <= r && r <= 'z' :
234+ return r
235+ case 'A' <= r && r <= 'Z' :
236+ return r
237+ case '0' <= r && r <= '9' :
238+ return r
239+ case r == '.' || r == '_' || r == '-' :
240+ return r
241+ }
242+ return '-'
243+ }, s )
244+ // Note: it's OK to use n as a byte index because the
245+ // above Map has eliminated all non-ASCII characters.
246+ if len (s ) <= n {
247+ return s
248+ }
249+ return s [:n ]
250+ }
0 commit comments