Skip to content

Commit 974f336

Browse files
author
Nimrod Shneor
committed
Add support for implicit paging in un/structured clients
1 parent 9e78e65 commit 974f336

File tree

4 files changed

+164
-0
lines changed

4 files changed

+164
-0
lines changed

pkg/client/client_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package client_test
1919
import (
2020
"context"
2121
"fmt"
22+
"strconv"
2223
"sync/atomic"
2324
"time"
2425

@@ -47,6 +48,14 @@ func deleteDeployment(ctx context.Context, dep *appsv1.Deployment, ns string) {
4748
}
4849
}
4950

51+
func deletePod(ctx context.Context, pod *corev1.Pod, ns string) {
52+
_, err := clientset.CoreV1().Pods(ns).Get(ctx, pod.Name, metav1.GetOptions{})
53+
if err == nil {
54+
err = clientset.CoreV1().Pods(ns).Delete(ctx, pod.Name, metav1.DeleteOptions{})
55+
Expect(err).NotTo(HaveOccurred())
56+
}
57+
}
58+
5059
func deleteNamespace(ctx context.Context, ns *corev1.Namespace) {
5160
ns, err := clientset.CoreV1().Namespaces().Get(ctx, ns.Name, metav1.GetOptions{})
5261
if err != nil {
@@ -2085,6 +2094,88 @@ var _ = Describe("Client", func() {
20852094
Expect(deps.Items[1].Name).To(Equal(dep4.Name))
20862095
}, serverSideTimeoutSeconds)
20872096

2097+
It("should filter results using page and limit options", func() {
2098+
buildPod := func(suffix string) *corev1.Pod {
2099+
return &corev1.Pod{
2100+
ObjectMeta: metav1.ObjectMeta{
2101+
Name: fmt.Sprintf("pod-%s", suffix),
2102+
},
2103+
Spec: corev1.PodSpec{
2104+
Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}},
2105+
},
2106+
}
2107+
}
2108+
2109+
By("creating 150 pods")
2110+
workLoad := 150
2111+
for workLoad > 0 {
2112+
pod := buildPod(strconv.Itoa(workLoad))
2113+
defer deletePod(ctx, pod, ns)
2114+
pod, err := clientset.
2115+
CoreV1().
2116+
Pods(ns).
2117+
Create(ctx, pod, metav1.CreateOptions{})
2118+
Expect(err).NotTo(HaveOccurred())
2119+
workLoad--
2120+
defer deletePod(ctx, pod, ns)
2121+
}
2122+
2123+
cl, err := client.New(cfg, client.Options{})
2124+
Expect(err).NotTo(HaveOccurred())
2125+
2126+
var offset int64
2127+
By("listing first one hundred pods when page=0 is used")
2128+
pods := &corev1.PodList{}
2129+
err = cl.List(context.Background(), pods,
2130+
client.Page{
2131+
Offset: &offset,
2132+
},
2133+
)
2134+
Expect(err).NotTo(HaveOccurred())
2135+
Expect(pods.Items).To(HaveLen(client.DefaultPageLimit))
2136+
Expect(pods.Continue).NotTo(BeEmpty())
2137+
Expect(pods.Items[0].Name).To(Equal("pod-1"))
2138+
2139+
By("listing the second page of results with page=1")
2140+
pods = &corev1.PodList{}
2141+
offset = 1
2142+
err = cl.List(context.Background(), pods,
2143+
client.Page{
2144+
Offset: &offset,
2145+
},
2146+
)
2147+
Expect(err).NotTo(HaveOccurred())
2148+
Expect(pods.Continue).To(BeEmpty())
2149+
Expect(pods.Items).To(HaveLen(50))
2150+
2151+
By("listing first page of fifty pods when page=0 and limit=50 are specified")
2152+
pods = &corev1.PodList{}
2153+
offset = 0
2154+
err = cl.List(context.Background(), pods,
2155+
client.Page{
2156+
Offset: &offset,
2157+
},
2158+
client.Limit(50),
2159+
)
2160+
Expect(err).NotTo(HaveOccurred())
2161+
Expect(pods.Continue).NotTo(BeEmpty())
2162+
Expect(pods.Items).To(HaveLen(50))
2163+
2164+
By("listing second page of fifty pods when page=1 and limit=50 are specified")
2165+
pods = &corev1.PodList{}
2166+
offset = 1
2167+
err = cl.List(context.Background(), pods,
2168+
client.Page{
2169+
Offset: &offset,
2170+
},
2171+
client.Limit(50),
2172+
)
2173+
Expect(err).NotTo(HaveOccurred())
2174+
Expect(pods.Continue).NotTo(BeEmpty())
2175+
Expect(pods.Items).To(HaveLen(50))
2176+
2177+
}, serverSideTimeoutSeconds)
2178+
20882179
PIt("should fail if the object doesn't have meta", func() {
20892180

20902181
})

pkg/client/options.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import (
2323
"k8s.io/apimachinery/pkg/selection"
2424
)
2525

26+
const (
27+
// DefaultPageLimit represents the default limit used when specifyinh the 'Page' ListOption.
28+
DefaultPageLimit = 100
29+
)
30+
2631
// {{{ "Functional" Option Interfaces
2732

2833
// CreateOption is some configuration that modifies options for a create request.
@@ -341,6 +346,14 @@ type ListOptions struct {
341346
// it has expired. This field is not supported if watch is true in the Raw ListOptions.
342347
Continue string
343348

349+
// Page is used to retrieve results at a certain offset from the server.
350+
// To avoid confusion paging start at index one.
351+
// The limit used as a single page size is one hundred items by default.
352+
// The server may reject requests for continuation tokens
353+
// it does not recognize and will return a 410 error if the token can no longer be used because
354+
// it has expired. This field is not supported if watch is true in the Raw ListOptions.
355+
Page Page
356+
344357
// Raw represents raw ListOptions, as passed to the API server. Note
345358
// that these may not be respected by all implementations of interface,
346359
// and the LabelSelector, FieldSelector, Limit and Continue fields are ignored.
@@ -521,6 +534,19 @@ func (c Continue) ApplyToList(opts *ListOptions) {
521534
opts.Continue = string(c)
522535
}
523536

537+
// Page sets a page offset for the result set from the server.
538+
// The limit used as a single page size is one hundred items by default.
539+
// Page does not implement DeleteAllOfOption interface because the server
540+
// does not support setting it for deletecollection operations.
541+
type Page struct {
542+
Offset *int64
543+
}
544+
545+
// ApplyToList applies this configuration to the given an List options.
546+
func (p Page) ApplyToList(opts *ListOptions) {
547+
opts.Page.Offset = p.Offset
548+
}
549+
524550
// }}}
525551

526552
// {{{ Update Options

pkg/client/typed_client.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,30 @@ func (c *typedClient) List(ctx context.Context, obj ObjectList, opts ...ListOpti
151151
}
152152
listOpts := ListOptions{}
153153
listOpts.ApplyOptions(opts)
154+
155+
// This is an implicit implementation of paging as kube only provides limit and
156+
// continuation token as a mechanism for retrieving large sets of results.
157+
// For more details, see the following:
158+
// https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks
159+
if listOpts.Page.Offset != nil {
160+
offset := *listOpts.Page.Offset
161+
if listOpts.Limit == 0 {
162+
Limit(DefaultPageLimit).ApplyToList(&listOpts)
163+
}
164+
for offset > 0 {
165+
err := r.Get().
166+
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
167+
Resource(r.resource()).
168+
VersionedParams(listOpts.AsListOptions(), c.paramCodec).
169+
Do(ctx).Into(obj)
170+
if err != nil {
171+
return err
172+
}
173+
listOpts.Continue = obj.GetContinue()
174+
offset--
175+
}
176+
}
177+
154178
return r.Get().
155179
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
156180
Resource(r.resource()).

pkg/client/unstructured_client.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,29 @@ func (uc *unstructuredClient) List(ctx context.Context, obj ObjectList, opts ...
213213
return err
214214
}
215215

216+
// This is an implicit implementation of paging as kube only provides a limit and
217+
// continuation token params as a mechanism for retrieving large sets of results.
218+
// For more details, see the following:
219+
// https://kubernetes.io/docs/reference/using-api/api-concepts/#retrieving-large-results-sets-in-chunks
220+
if listOpts.Page.Offset != nil {
221+
offset := *listOpts.Page.Offset
222+
if listOpts.Limit == 0 {
223+
Limit(DefaultPageLimit).ApplyToList(&listOpts)
224+
}
225+
for offset > 0 {
226+
err := r.Get().
227+
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
228+
Resource(r.resource()).
229+
VersionedParams(listOpts.AsListOptions(), uc.paramCodec).
230+
Do(ctx).Into(obj)
231+
if err != nil {
232+
return err
233+
}
234+
listOpts.Continue = obj.GetContinue()
235+
offset--
236+
}
237+
}
238+
216239
return r.Get().
217240
NamespaceIfScoped(listOpts.Namespace, r.isNamespaced()).
218241
Resource(r.resource()).

0 commit comments

Comments
 (0)