diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 2c45134f35..28460fb7e4 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1904,6 +1904,89 @@ var _ = Describe("Client", func() { close(done) }, serverSideTimeoutSeconds) + It("should filter results using limit and continue options", func() { + + makeDeployment := func(suffix string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("deployment-%s", suffix), + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, + }, + }, + } + } + + By("creating 4 deployments") + dep1 := makeDeployment("1") + dep1, err := clientset.AppsV1().Deployments(ns).Create(dep1) + Expect(err).NotTo(HaveOccurred()) + defer deleteDeployment(dep1, ns) + + dep2 := makeDeployment("2") + dep2, err = clientset.AppsV1().Deployments(ns).Create(dep2) + Expect(err).NotTo(HaveOccurred()) + defer deleteDeployment(dep2, ns) + + dep3 := makeDeployment("3") + dep3, err = clientset.AppsV1().Deployments(ns).Create(dep3) + Expect(err).NotTo(HaveOccurred()) + defer deleteDeployment(dep3, ns) + + dep4 := makeDeployment("4") + dep4, err = clientset.AppsV1().Deployments(ns).Create(dep4) + Expect(err).NotTo(HaveOccurred()) + defer deleteDeployment(dep4, ns) + + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("listing 1 deployment when limit=1 is used") + deps := &appsv1.DeploymentList{} + err = cl.List(context.Background(), deps, + client.Limit(1), + ) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).To(HaveLen(1)) + Expect(deps.Continue).NotTo(BeEmpty()) + Expect(deps.Items[0].Name).To(Equal(dep1.Name)) + + continueToken := deps.Continue + + By("listing the next deployment when previous continuation token is used and limit=1") + deps = &appsv1.DeploymentList{} + err = cl.List(context.Background(), deps, + client.Limit(1), + client.Continue(continueToken), + ) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).To(HaveLen(1)) + Expect(deps.Continue).NotTo(BeEmpty()) + Expect(deps.Items[0].Name).To(Equal(dep2.Name)) + + continueToken = deps.Continue + + By("listing the 2 remaining deployments when previous continuation token is used without a limit") + deps = &appsv1.DeploymentList{} + err = cl.List(context.Background(), deps, + client.Continue(continueToken), + ) + Expect(err).NotTo(HaveOccurred()) + + Expect(deps.Items).To(HaveLen(2)) + Expect(deps.Continue).To(BeEmpty()) + Expect(deps.Items[0].Name).To(Equal(dep3.Name)) + Expect(deps.Items[1].Name).To(Equal(dep4.Name)) + }, serverSideTimeoutSeconds) + PIt("should fail if the object doesn't have meta", func() { }) @@ -2309,11 +2392,15 @@ var _ = Describe("Client", func() { client.MatchingField("field1", "bar"), client.InNamespace("test-namespace"), client.MatchingLabels{"foo": "bar"}, + client.Limit(1), + client.Continue("foo"), }) mlo := lo.AsListOptions() Expect(mlo).NotTo(BeNil()) Expect(mlo.LabelSelector).To(Equal("foo=bar")) Expect(mlo.FieldSelector).To(Equal("field1=bar")) + Expect(mlo.Limit).To(Equal(int64(1))) + Expect(mlo.Continue).To(Equal("foo")) }) It("should be populated by MatchingLabels", func() { @@ -2343,6 +2430,58 @@ var _ = Describe("Client", func() { do = &client.ListOptions{} Expect(do.AsListOptions()).To(Equal(&metav1.ListOptions{})) }) + + It("should be populated by Limit", func() { + lo := &client.ListOptions{} + client.Limit(1).ApplyToList(lo) + Expect(lo).NotTo(BeNil()) + Expect(lo.Limit).To(Equal(int64(1))) + }) + + It("should ignore Limit when converted to metav1.ListOptions and watch is true", func() { + lo := &client.ListOptions{ + Raw: &metav1.ListOptions{Watch: true}, + } + lo.ApplyOptions([]client.ListOption{ + client.Limit(1), + }) + mlo := lo.AsListOptions() + Expect(mlo).NotTo(BeNil()) + Expect(mlo.Limit).To(BeZero()) + }) + + It("should be populated by Continue", func() { + lo := &client.ListOptions{} + client.Continue("foo").ApplyToList(lo) + Expect(lo).NotTo(BeNil()) + Expect(lo.Continue).To(Equal("foo")) + }) + + It("should ignore Continue token when converted to metav1.ListOptions and watch is true", func() { + lo := &client.ListOptions{ + Raw: &metav1.ListOptions{Watch: true}, + } + lo.ApplyOptions([]client.ListOption{ + client.Continue("foo"), + }) + mlo := lo.AsListOptions() + Expect(mlo).NotTo(BeNil()) + Expect(mlo.Continue).To(BeEmpty()) + }) + + It("should ignore both Limit and Continue token when converted to metav1.ListOptions and watch is true", func() { + lo := &client.ListOptions{ + Raw: &metav1.ListOptions{Watch: true}, + } + lo.ApplyOptions([]client.ListOption{ + client.Limit(1), + client.Continue("foo"), + }) + mlo := lo.AsListOptions() + Expect(mlo).NotTo(BeNil()) + Expect(mlo.Limit).To(BeZero()) + Expect(mlo.Continue).To(BeEmpty()) + }) }) Describe("UpdateOptions", func() { diff --git a/pkg/client/options.go b/pkg/client/options.go index 4007a67657..27febf037b 100644 --- a/pkg/client/options.go +++ b/pkg/client/options.go @@ -267,9 +267,20 @@ type ListOptions struct { // non-namespaced objects, or to list across all namespaces. Namespace string + // Limit specifies the maximum number of results to return from the server. The server may + // not support this field on all resource types, but if it does and more results remain it + // will set the continue field on the returned list object. This field is not supported if watch + // is true in the Raw ListOptions. + Limit int64 + // Continue is a token returned by the server that lets a client retrieve chunks of results + // from the server by specifying limit. The server may reject requests for continuation tokens + // it does not recognize and will return a 410 error if the token can no longer be used because + // it has expired. This field is not supported if watch is true in the Raw ListOptions. + Continue string + // Raw represents raw ListOptions, as passed to the API server. Note // that these may not be respected by all implementations of interface, - // and the LabelSelector and FieldSelector fields are ignored. + // and the LabelSelector, FieldSelector, Limit and Continue fields are ignored. Raw *metav1.ListOptions } @@ -288,6 +299,10 @@ func (o *ListOptions) AsListOptions() *metav1.ListOptions { if o.FieldSelector != nil { o.Raw.FieldSelector = o.FieldSelector.String() } + if !o.Raw.Watch { + o.Raw.Limit = o.Limit + o.Raw.Continue = o.Continue + } return o.Raw } @@ -346,6 +361,24 @@ func (n InNamespace) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { n.ApplyToList(&opts.ListOptions) } +// Limit specifies the maximum number of results to return from the server. +// Limit does not implement DeleteAllOfOption interface because the server +// does not support setting it for deletecollection operations. +type Limit int64 + +func (l Limit) ApplyToList(opts *ListOptions) { + opts.Limit = int64(l) +} + +// Continue sets a continuation token to retrieve chunks of results when using limit. +// Continue does not implement DeleteAllOfOption interface because the server +// does not support setting it for deletecollection operations. +type Continue string + +func (c Continue) ApplyToList(opts *ListOptions) { + opts.Continue = string(c) +} + // }}} // {{{ Update Options