diff --git a/e2e/src/test/java/io/kubernetes/client/e2e/dynamic/DynamicApiTest.java b/e2e/src/test/java/io/kubernetes/client/e2e/dynamic/DynamicApiTest.java index 3c76ff7a03..aac5a314ef 100644 --- a/e2e/src/test/java/io/kubernetes/client/e2e/dynamic/DynamicApiTest.java +++ b/e2e/src/test/java/io/kubernetes/client/e2e/dynamic/DynamicApiTest.java @@ -19,7 +19,10 @@ import io.kubernetes.client.openapi.models.V1Namespace; import io.kubernetes.client.openapi.models.V1ObjectMeta; import io.kubernetes.client.util.ClientBuilder; +import io.kubernetes.client.util.generic.KubernetesApiResponse; +import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesListObject; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.Dynamics; import org.junit.jupiter.api.Test; @@ -41,4 +44,35 @@ void dynamicApiCreateAndDeleteNamespace() throws Exception { dynamicApi.delete("e2e-dynamic").throwsApiException().getObject(); assertThat(deleted).isNotNull(); } + + @Test + void dynamicApiListWithResourceVersionMatchAndWatchBookmarks() throws Exception { + ApiClient client = ClientBuilder.defaultClient(); + DynamicKubernetesApi dynamicApi = + new DynamicKubernetesApi("", "v1", "namespaces", client); + String namespaceName = "e2e-dynamic-list-options"; + V1Namespace namespace = new V1Namespace().metadata(new V1ObjectMeta().name(namespaceName)); + + DynamicKubernetesObject createdNamespace = + dynamicApi.create(Dynamics.newFromJson(JSON.serialize(namespace))).getObject(); + assertThat(createdNamespace).isNotNull(); + + try { + KubernetesApiResponse listResponse = + dynamicApi.list( + ListOptions.builder() + .fieldSelector("metadata.name=" + namespaceName) + .resourceVersion(createdNamespace.getMetadata().getResourceVersion()) + .resourceVersionMatch("NotOlderThan") + .allowWatchBookmarks(true) + .build()); + + assertThat(listResponse.isSuccess()).isTrue(); + assertThat(listResponse.getObject()).isNotNull(); + assertThat(listResponse.getObject().getItems()) + .anySatisfy(item -> assertThat(item.getMetadata().getName()).isEqualTo(namespaceName)); + } finally { + dynamicApi.delete(namespaceName).throwsApiException(); + } + } } diff --git a/util/src/main/java/io/kubernetes/client/util/generic/GenericKubernetesApi.java b/util/src/main/java/io/kubernetes/client/util/generic/GenericKubernetesApi.java index 21360a7bb0..53893b937a 100644 --- a/util/src/main/java/io/kubernetes/client/util/generic/GenericKubernetesApi.java +++ b/util/src/main/java/io/kubernetes/client/util/generic/GenericKubernetesApi.java @@ -579,11 +579,13 @@ private CallBuilder makeClusterListCallBuilder(final ListOptions listOptions) { adaptListCall( customObjectsApi.getApiClient(), customObjectsApi.listClusterCustomObject( this.apiGroup, this.apiVersion, this.resourcePlural) + .allowWatchBookmarks(listOptions.getAllowWatchBookmarks()) ._continue(listOptions.getContinue()) .fieldSelector(listOptions.getFieldSelector()) .labelSelector(listOptions.getLabelSelector()) .limit(listOptions.getLimit()) .resourceVersion(listOptions.getResourceVersion()) + .resourceVersionMatch(listOptions.getResourceVersionMatch()) .timeoutSeconds(listOptions.getTimeoutSeconds()) .watch(false) .buildCall(null), @@ -626,11 +628,13 @@ private CallBuilder makeNamespacedListCallBuilder( adaptListCall( customObjectsApi.getApiClient(), customObjectsApi.listNamespacedCustomObject(this.apiGroup, this.apiVersion, namespace, this.resourcePlural) + .allowWatchBookmarks(listOptions.getAllowWatchBookmarks()) ._continue(listOptions.getContinue()) .fieldSelector(listOptions.getFieldSelector()) .labelSelector(listOptions.getLabelSelector()) .limit(listOptions.getLimit()) .resourceVersion(listOptions.getResourceVersion()) + .resourceVersionMatch(listOptions.getResourceVersionMatch()) .timeoutSeconds(listOptions.getTimeoutSeconds()) .watch(false) .buildCall(null), @@ -1229,11 +1233,13 @@ public Watchable watch(final ListOptions listOptions) throws ApiExcepti this.apiGroup, this.apiVersion, this.resourcePlural) + .allowWatchBookmarks(listOptions.getAllowWatchBookmarks()) ._continue(listOptions.getContinue()) .fieldSelector(listOptions.getFieldSelector()) .labelSelector(listOptions.getLabelSelector()) .limit(listOptions.getLimit()) .resourceVersion(listOptions.getResourceVersion()) + .resourceVersionMatch(listOptions.getResourceVersionMatch()) .timeoutSeconds(listOptions.getTimeoutSeconds()) .watch(true) .buildCall(null); @@ -1264,11 +1270,13 @@ public Watchable watch(String namespace, final ListOptions listOptions) this.apiVersion, namespace, this.resourcePlural) + .allowWatchBookmarks(listOptions.getAllowWatchBookmarks()) ._continue(listOptions.getContinue()) .fieldSelector(listOptions.getFieldSelector()) .labelSelector(listOptions.getLabelSelector()) .limit(listOptions.getLimit()) .resourceVersion(listOptions.getResourceVersion()) + .resourceVersionMatch(listOptions.getResourceVersionMatch()) .timeoutSeconds(listOptions.getTimeoutSeconds()) .watch(true) .buildCall(null); diff --git a/util/src/main/java/io/kubernetes/client/util/generic/options/ListOptions.java b/util/src/main/java/io/kubernetes/client/util/generic/options/ListOptions.java index aa386573b8..4bab93b0e1 100644 --- a/util/src/main/java/io/kubernetes/client/util/generic/options/ListOptions.java +++ b/util/src/main/java/io/kubernetes/client/util/generic/options/ListOptions.java @@ -24,6 +24,12 @@ public class ListOptions { @SerializedName("resourceVersion") private String resourceVersion; + @SerializedName("resourceVersionMatch") + private String resourceVersionMatch; + + @SerializedName("allowWatchBookmarks") + private Boolean allowWatchBookmarks; + @SerializedName("timeoutSeconds") private Integer timeoutSeconds; @@ -36,6 +42,10 @@ public class ListOptions { @SerializedName("isPartialObjectMetadataListRequest") private Boolean isPartialObjectMetadataListRequest; + public static Builder builder() { + return new Builder(); + } + public ListOptions fieldSelector(String fieldSelector) { this.fieldSelector = fieldSelector; return this; @@ -75,6 +85,32 @@ public void setResourceVersion(String resourceVersion) { this.resourceVersion = resourceVersion; } + public ListOptions resourceVersionMatch(String resourceVersionMatch) { + this.resourceVersionMatch = resourceVersionMatch; + return this; + } + + public String getResourceVersionMatch() { + return resourceVersionMatch; + } + + public void setResourceVersionMatch(String resourceVersionMatch) { + this.resourceVersionMatch = resourceVersionMatch; + } + + public ListOptions allowWatchBookmarks(Boolean allowWatchBookmarks) { + this.allowWatchBookmarks = allowWatchBookmarks; + return this; + } + + public Boolean getAllowWatchBookmarks() { + return allowWatchBookmarks; + } + + public void setAllowWatchBookmarks(Boolean allowWatchBookmarks) { + this.allowWatchBookmarks = allowWatchBookmarks; + } + public ListOptions limit(Integer limit) { this.limit = limit; return this; @@ -127,4 +163,71 @@ public Boolean isPartialObjectMetadataListRequest() { public void setPartialObjectMetadataListRequest(Boolean isPartialObjectMetadataListRequest) { this.isPartialObjectMetadataListRequest = isPartialObjectMetadataListRequest; } + + public static final class Builder { + private final ListOptions options; + + private Builder() { + this.options = new ListOptions(); + } + + public Builder fieldSelector(String fieldSelector) { + options.fieldSelector(fieldSelector); + return this; + } + + public Builder labelSelector(String labelSelector) { + options.labelSelector(labelSelector); + return this; + } + + public Builder resourceVersion(String resourceVersion) { + options.resourceVersion(resourceVersion); + return this; + } + + public Builder resourceVersionMatch(String resourceVersionMatch) { + options.resourceVersionMatch(resourceVersionMatch); + return this; + } + + public Builder allowWatchBookmarks(Boolean allowWatchBookmarks) { + options.allowWatchBookmarks(allowWatchBookmarks); + return this; + } + + public Builder timeoutSeconds(Integer timeoutSeconds) { + options.timeoutSeconds(timeoutSeconds); + return this; + } + + public Builder limit(Integer limit) { + options.limit(limit); + return this; + } + + public Builder _continue(String _continue) { + options._continue(_continue); + return this; + } + + public Builder isPartialObjectMetadataListRequest(Boolean partialMetadata) { + options.isPartialObjectMetadataListRequest(partialMetadata); + return this; + } + + public ListOptions build() { + ListOptions built = new ListOptions(); + built.setFieldSelector(options.getFieldSelector()); + built.setLabelSelector(options.getLabelSelector()); + built.setResourceVersion(options.getResourceVersion()); + built.setResourceVersionMatch(options.getResourceVersionMatch()); + built.setAllowWatchBookmarks(options.getAllowWatchBookmarks()); + built.setTimeoutSeconds(options.getTimeoutSeconds()); + built.setLimit(options.getLimit()); + built.setContinue(options.getContinue()); + built.setPartialObjectMetadataListRequest(options.isPartialObjectMetadataListRequest()); + return built; + } + } } diff --git a/util/src/test/java/io/kubernetes/client/util/generic/GenericKubernetesApiTest.java b/util/src/test/java/io/kubernetes/client/util/generic/GenericKubernetesApiTest.java index d423cf1a88..b262a03c91 100644 --- a/util/src/test/java/io/kubernetes/client/util/generic/GenericKubernetesApiTest.java +++ b/util/src/test/java/io/kubernetes/client/util/generic/GenericKubernetesApiTest.java @@ -115,6 +115,32 @@ void listNamespacedJobReturningObject() { apiServer.verify(1, getRequestedFor(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs"))); } + @Test + void listNamespacedJobWithResourceVersionMatchAndWatchBookmarks() { + V1JobList jobList = new V1JobList().kind("JobList").metadata(new V1ListMeta()); + + apiServer.stubFor( + get(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs")) + .willReturn(aResponse().withStatus(200).withBody(json.serialize(jobList)))); + + KubernetesApiResponse jobListResp = + jobClient.list( + "default", + new ListOptions() + .resourceVersion("123") + .resourceVersionMatch("NotOlderThan") + .allowWatchBookmarks(true)); + assertThat(jobListResp.isSuccess()).isTrue(); + assertThat(jobListResp.getObject()).isEqualTo(jobList); + assertThat(jobListResp.getStatus()).isNull(); + apiServer.verify( + 1, + getRequestedFor(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs")) + .withQueryParam("resourceVersion", equalTo("123")) + .withQueryParam("resourceVersionMatch", equalTo("NotOlderThan")) + .withQueryParam("allowWatchBookmarks", equalTo("true"))); + } + @Test void listNamespacedJobWithPartialMetadataObjectListHeader() { V1JobList jobList = @@ -279,6 +305,30 @@ void watchNamespacedJobReturningObject() throws ApiException { .withQueryParam("watch", equalTo("true"))); } + @Test + void watchNamespacedJobWithResourceVersionMatchAndWatchBookmarks() throws ApiException { + V1JobList jobList = new V1JobList().kind("JobList").metadata(new V1ListMeta()); + + apiServer.stubFor( + get(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs")) + .willReturn(aResponse().withStatus(200).withBody(json.serialize(jobList)))); + Watchable jobListWatch = + jobClient.watch( + "default", + new ListOptions() + .resourceVersion("123") + .resourceVersionMatch("NotOlderThan") + .allowWatchBookmarks(true)); + assertThat((Object) jobListWatch).isNotNull(); + apiServer.verify( + 1, + getRequestedFor(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs")) + .withQueryParam("watch", equalTo("true")) + .withQueryParam("resourceVersion", equalTo("123")) + .withQueryParam("resourceVersionMatch", equalTo("NotOlderThan")) + .withQueryParam("allowWatchBookmarks", equalTo("true"))); + } + @Test void readTimeoutShouldThrowException() { ApiClient apiClient = diff --git a/util/src/test/java/io/kubernetes/client/util/generic/options/ListOptionsBuilderTest.java b/util/src/test/java/io/kubernetes/client/util/generic/options/ListOptionsBuilderTest.java new file mode 100644 index 0000000000..9a2f5f012d --- /dev/null +++ b/util/src/test/java/io/kubernetes/client/util/generic/options/ListOptionsBuilderTest.java @@ -0,0 +1,46 @@ +/* +Copyright 2026 The Kubernetes Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package io.kubernetes.client.util.generic.options; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ListOptionsBuilderTest { + + @Test + void listOptionsBuilderShouldPopulateAllFields() { + ListOptions options = + ListOptions.builder() + .fieldSelector("metadata.name=test") + .labelSelector("app=test") + .resourceVersion("100") + .resourceVersionMatch("NotOlderThan") + .allowWatchBookmarks(true) + .timeoutSeconds(30) + .limit(50) + ._continue("next-page-token") + .isPartialObjectMetadataListRequest(true) + .build(); + + assertThat(options.getFieldSelector()).isEqualTo("metadata.name=test"); + assertThat(options.getLabelSelector()).isEqualTo("app=test"); + assertThat(options.getResourceVersion()).isEqualTo("100"); + assertThat(options.getResourceVersionMatch()).isEqualTo("NotOlderThan"); + assertThat(options.getAllowWatchBookmarks()).isTrue(); + assertThat(options.getTimeoutSeconds()).isEqualTo(30); + assertThat(options.getLimit()).isEqualTo(50); + assertThat(options.getContinue()).isEqualTo("next-page-token"); + assertThat(options.isPartialObjectMetadataListRequest()).isTrue(); + } +}