k8s.io/client-go@v0.22.2/tools/pager/pager.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package pager 18 19 import ( 20 "context" 21 "fmt" 22 23 "k8s.io/apimachinery/pkg/api/errors" 24 "k8s.io/apimachinery/pkg/api/meta" 25 metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 29 ) 30 31 const defaultPageSize = 500 32 const defaultPageBufferSize = 10 33 34 // ListPageFunc returns a list object for the given list options. 35 type ListPageFunc func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) 36 37 // SimplePageFunc adapts a context-less list function into one that accepts a context. 38 func SimplePageFunc(fn func(opts metav1.ListOptions) (runtime.Object, error)) ListPageFunc { 39 return func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { 40 return fn(opts) 41 } 42 } 43 44 // ListPager assists client code in breaking large list queries into multiple 45 // smaller chunks of PageSize or smaller. PageFn is expected to accept a 46 // metav1.ListOptions that supports paging and return a list. The pager does 47 // not alter the field or label selectors on the initial options list. 48 type ListPager struct { 49 PageSize int64 50 PageFn ListPageFunc 51 52 FullListIfExpired bool 53 54 // Number of pages to buffer 55 PageBufferSize int32 56 } 57 58 // New creates a new pager from the provided pager function using the default 59 // options. It will fall back to a full list if an expiration error is encountered 60 // as a last resort. 61 func New(fn ListPageFunc) *ListPager { 62 return &ListPager{ 63 PageSize: defaultPageSize, 64 PageFn: fn, 65 FullListIfExpired: true, 66 PageBufferSize: defaultPageBufferSize, 67 } 68 } 69 70 // TODO: introduce other types of paging functions - such as those that retrieve from a list 71 // of namespaces. 72 73 // List returns a single list object, but attempts to retrieve smaller chunks from the 74 // server to reduce the impact on the server. If the chunk attempt fails, it will load 75 // the full list instead. The Limit field on options, if unset, will default to the page size. 76 func (p *ListPager) List(ctx context.Context, options metav1.ListOptions) (runtime.Object, bool, error) { 77 if options.Limit == 0 { 78 options.Limit = p.PageSize 79 } 80 requestedResourceVersion := options.ResourceVersion 81 var list *metainternalversion.List 82 paginatedResult := false 83 84 for { 85 select { 86 case <-ctx.Done(): 87 return nil, paginatedResult, ctx.Err() 88 default: 89 } 90 91 obj, err := p.PageFn(ctx, options) 92 if err != nil { 93 // Only fallback to full list if an "Expired" errors is returned, FullListIfExpired is true, and 94 // the "Expired" error occurred in page 2 or later (since full list is intended to prevent a pager.List from 95 // failing when the resource versions is established by the first page request falls out of the compaction 96 // during the subsequent list requests). 97 if !errors.IsResourceExpired(err) || !p.FullListIfExpired || options.Continue == "" { 98 return nil, paginatedResult, err 99 } 100 // the list expired while we were processing, fall back to a full list at 101 // the requested ResourceVersion. 102 options.Limit = 0 103 options.Continue = "" 104 options.ResourceVersion = requestedResourceVersion 105 result, err := p.PageFn(ctx, options) 106 return result, paginatedResult, err 107 } 108 m, err := meta.ListAccessor(obj) 109 if err != nil { 110 return nil, paginatedResult, fmt.Errorf("returned object must be a list: %v", err) 111 } 112 113 // exit early and return the object we got if we haven't processed any pages 114 if len(m.GetContinue()) == 0 && list == nil { 115 return obj, paginatedResult, nil 116 } 117 118 // initialize the list and fill its contents 119 if list == nil { 120 list = &metainternalversion.List{Items: make([]runtime.Object, 0, options.Limit+1)} 121 list.ResourceVersion = m.GetResourceVersion() 122 list.SelfLink = m.GetSelfLink() 123 } 124 if err := meta.EachListItem(obj, func(obj runtime.Object) error { 125 list.Items = append(list.Items, obj) 126 return nil 127 }); err != nil { 128 return nil, paginatedResult, err 129 } 130 131 // if we have no more items, return the list 132 if len(m.GetContinue()) == 0 { 133 return list, paginatedResult, nil 134 } 135 136 // set the next loop up 137 options.Continue = m.GetContinue() 138 // Clear the ResourceVersion on the subsequent List calls to avoid the 139 // `specifying resource version is not allowed when using continue` error. 140 // See https://github.com/kubernetes/kubernetes/issues/85221#issuecomment-553748143. 141 options.ResourceVersion = "" 142 // At this point, result is already paginated. 143 paginatedResult = true 144 } 145 } 146 147 // EachListItem fetches runtime.Object items using this ListPager and invokes fn on each item. If 148 // fn returns an error, processing stops and that error is returned. If fn does not return an error, 149 // any error encountered while retrieving the list from the server is returned. If the context 150 // cancels or times out, the context error is returned. Since the list is retrieved in paginated 151 // chunks, an "Expired" error (metav1.StatusReasonExpired) may be returned if the pagination list 152 // requests exceed the expiration limit of the apiserver being called. 153 // 154 // Items are retrieved in chunks from the server to reduce the impact on the server with up to 155 // ListPager.PageBufferSize chunks buffered concurrently in the background. 156 func (p *ListPager) EachListItem(ctx context.Context, options metav1.ListOptions, fn func(obj runtime.Object) error) error { 157 return p.eachListChunkBuffered(ctx, options, func(obj runtime.Object) error { 158 return meta.EachListItem(obj, fn) 159 }) 160 } 161 162 // eachListChunkBuffered fetches runtimeObject list chunks using this ListPager and invokes fn on 163 // each list chunk. If fn returns an error, processing stops and that error is returned. If fn does 164 // not return an error, any error encountered while retrieving the list from the server is 165 // returned. If the context cancels or times out, the context error is returned. Since the list is 166 // retrieved in paginated chunks, an "Expired" error (metav1.StatusReasonExpired) may be returned if 167 // the pagination list requests exceed the expiration limit of the apiserver being called. 168 // 169 // Up to ListPager.PageBufferSize chunks are buffered concurrently in the background. 170 func (p *ListPager) eachListChunkBuffered(ctx context.Context, options metav1.ListOptions, fn func(obj runtime.Object) error) error { 171 if p.PageBufferSize < 0 { 172 return fmt.Errorf("ListPager.PageBufferSize must be >= 0, got %d", p.PageBufferSize) 173 } 174 175 // Ensure background goroutine is stopped if this call exits before all list items are 176 // processed. Cancelation error from this deferred cancel call is never returned to caller; 177 // either the list result has already been sent to bgResultC or the fn error is returned and 178 // the cancelation error is discarded. 179 ctx, cancel := context.WithCancel(ctx) 180 defer cancel() 181 182 chunkC := make(chan runtime.Object, p.PageBufferSize) 183 bgResultC := make(chan error, 1) 184 go func() { 185 defer utilruntime.HandleCrash() 186 187 var err error 188 defer func() { 189 close(chunkC) 190 bgResultC <- err 191 }() 192 err = p.eachListChunk(ctx, options, func(chunk runtime.Object) error { 193 select { 194 case chunkC <- chunk: // buffer the chunk, this can block 195 case <-ctx.Done(): 196 return ctx.Err() 197 } 198 return nil 199 }) 200 }() 201 202 for o := range chunkC { 203 err := fn(o) 204 if err != nil { 205 return err // any fn error should be returned immediately 206 } 207 } 208 // promote the results of our background goroutine to the foreground 209 return <-bgResultC 210 } 211 212 // eachListChunk fetches runtimeObject list chunks using this ListPager and invokes fn on each list 213 // chunk. If fn returns an error, processing stops and that error is returned. If fn does not return 214 // an error, any error encountered while retrieving the list from the server is returned. If the 215 // context cancels or times out, the context error is returned. Since the list is retrieved in 216 // paginated chunks, an "Expired" error (metav1.StatusReasonExpired) may be returned if the 217 // pagination list requests exceed the expiration limit of the apiserver being called. 218 func (p *ListPager) eachListChunk(ctx context.Context, options metav1.ListOptions, fn func(obj runtime.Object) error) error { 219 if options.Limit == 0 { 220 options.Limit = p.PageSize 221 } 222 for { 223 select { 224 case <-ctx.Done(): 225 return ctx.Err() 226 default: 227 } 228 229 obj, err := p.PageFn(ctx, options) 230 if err != nil { 231 return err 232 } 233 m, err := meta.ListAccessor(obj) 234 if err != nil { 235 return fmt.Errorf("returned object must be a list: %v", err) 236 } 237 if err := fn(obj); err != nil { 238 return err 239 } 240 // if we have no more items, return. 241 if len(m.GetContinue()) == 0 { 242 return nil 243 } 244 // set the next loop up 245 options.Continue = m.GetContinue() 246 } 247 }