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  }