From f927cf0bd6a6a321743567f18fc923f9a5d5b3e5 Mon Sep 17 00:00:00 2001 From: Brendan Burns <5751682+brendandburns@users.noreply.github.com> Date: Wed, 21 Jan 2026 23:06:52 +0000 Subject: [PATCH 1/2] feat(util): add ResourceClient for fluent resource operations Add a utility class that provides fabric8-style fluent DSL for Kubernetes resource operations. Features include: - Fluent chaining: inNamespace(), withName(), withLabel() - CRUD operations: get(), list(), create(), update(), delete() - createOrReplace() for upsert semantics - serverSideApply() integration - waitUntilReady() and waitUntilCondition() support - Immutable client instances for thread safety Also adds comprehensive unit tests covering all operations, fluent interface chaining, and error handling. --- .../client/util/ResourceClient.java | 612 ++++++++++++++++++ .../client/util/ResourceClientTest.java | 460 +++++++++++++ 2 files changed, 1072 insertions(+) create mode 100644 util/src/main/java/io/kubernetes/client/util/ResourceClient.java create mode 100644 util/src/test/java/io/kubernetes/client/util/ResourceClientTest.java diff --git a/util/src/main/java/io/kubernetes/client/util/ResourceClient.java b/util/src/main/java/io/kubernetes/client/util/ResourceClient.java new file mode 100644 index 0000000000..25a268ad0a --- /dev/null +++ b/util/src/main/java/io/kubernetes/client/util/ResourceClient.java @@ -0,0 +1,612 @@ +/* +Copyright 2024 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; + +import io.kubernetes.client.common.KubernetesListObject; +import io.kubernetes.client.common.KubernetesObject; +import io.kubernetes.client.custom.V1Patch; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1DeleteOptions; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import io.kubernetes.client.util.generic.KubernetesApiResponse; +import io.kubernetes.client.util.generic.options.CreateOptions; +import io.kubernetes.client.util.generic.options.DeleteOptions; +import io.kubernetes.client.util.generic.options.GetOptions; +import io.kubernetes.client.util.generic.options.ListOptions; +import io.kubernetes.client.util.generic.options.PatchOptions; +import io.kubernetes.client.util.generic.options.UpdateOptions; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * Fluent DSL for Kubernetes resource operations, similar to fabric8 Kubernetes client. + * Provides a chainable, intuitive API for CRUD operations on Kubernetes resources. + * + *
Example usage: + *
{@code
+ * // Create a client for pods
+ * ResourceClient pods =
+ * ResourceClient.create(apiClient, V1Pod.class, V1PodList.class, "", "v1", "pods");
+ *
+ * // Get a pod in a namespace
+ * V1Pod pod = pods.inNamespace("default").withName("my-pod").get();
+ *
+ * // List all pods with a label
+ * V1PodList podList = pods.inNamespace("default")
+ * .withLabel("app", "myapp")
+ * .list();
+ *
+ * // Create or replace a pod
+ * V1Pod created = pods.inNamespace("default").createOrReplace(myPod);
+ *
+ * // Delete a pod
+ * pods.inNamespace("default").withName("my-pod").delete();
+ *
+ * // Wait until ready
+ * V1Pod ready = pods.inNamespace("default")
+ * .withName("my-pod")
+ * .waitUntilReady(Duration.ofMinutes(5));
+ *
+ * // Edit a resource
+ * V1Pod edited = pods.inNamespace("default")
+ * .withName("my-pod")
+ * .edit(pod -> {
+ * pod.getMetadata().getLabels().put("newlabel", "value");
+ * return pod;
+ * });
+ * }
+ *
+ * @param Example usage: *
{@code
* // Create a client for pods
- * ResourceClient pods =
+ * ResourceClient pods =
* ResourceClient.create(apiClient, V1Pod.class, V1PodList.class, "", "v1", "pods");
- *
+ *
* // Get a pod in a namespace
* V1Pod pod = pods.inNamespace("default").withName("my-pod").get();
- *
+ *
* // List all pods with a label
* V1PodList podList = pods.inNamespace("default")
* .withLabel("app", "myapp")
* .list();
- *
+ *
* // Create or replace a pod
* V1Pod created = pods.inNamespace("default").createOrReplace(myPod);
- *
+ *
* // Delete a pod
* pods.inNamespace("default").withName("my-pod").delete();
- *
+ *
* // Wait until ready
* V1Pod ready = pods.inNamespace("default")
* .withName("my-pod")
* .waitUntilReady(Duration.ofMinutes(5));
- *
+ *
* // Edit a resource
* V1Pod edited = pods.inNamespace("default")
* .withName("my-pod")
@@ -536,8 +536,8 @@ public void waitUntilDeleted(Duration timeout)
final String resourceName = name;
WaitUtils.waitUntilDeleted(
() -> {
- KubernetesApiResponse response = ns != null
- ? api.get(ns, resourceName)
+ KubernetesApiResponse response = ns != null
+ ? api.get(ns, resourceName)
: api.get(resourceName);
return response.isSuccess() ? response.getObject() : null;
},
@@ -557,8 +557,8 @@ public CompletableFuture waitUntilReadyAsync(Duration timeout) {
final String resourceName = name;
return WaitUtils.waitUntilReadyAsync(
() -> {
- KubernetesApiResponse response = ns != null
- ? api.get(ns, resourceName)
+ KubernetesApiResponse response = ns != null
+ ? api.get(ns, resourceName)
: api.get(resourceName);
return response.isSuccess() ? response.getObject() : null;
},
@@ -580,8 +580,8 @@ public CompletableFuture waitUntilConditionAsync(Predicate con
final String resourceName = name;
return WaitUtils.waitUntilConditionAsync(
() -> {
- KubernetesApiResponse response = ns != null
- ? api.get(ns, resourceName)
+ KubernetesApiResponse response = ns != null
+ ? api.get(ns, resourceName)
: api.get(resourceName);
return response.isSuccess() ? response.getObject() : null;
},
diff --git a/util/src/test/java/io/kubernetes/client/util/ResourceClientTest.java b/util/src/test/java/io/kubernetes/client/util/ResourceClientTest.java
index cdf58154b8..097f2a26f1 100644
--- a/util/src/test/java/io/kubernetes/client/util/ResourceClientTest.java
+++ b/util/src/test/java/io/kubernetes/client/util/ResourceClientTest.java
@@ -209,7 +209,7 @@ void create_clusterScopedResource_createsResource() throws ApiException {
void update_namespacedPod_updatesResource() throws ApiException {
V1Pod pod = createPod("existing-pod", "default");
pod.getMetadata().setResourceVersion("12345");
-
+
apiServer.stubFor(
put(urlPathEqualTo("/api/v1/namespaces/default/pods/existing-pod"))
.willReturn(aResponse()
@@ -322,7 +322,7 @@ void fluentInterface_chainedCalls_preservesState() throws ApiException {
@Test
void inNamespace_returnsNewClient() {
ResourceClient namespacedClient = podClient.inNamespace("my-namespace");
-
+
assertThat(namespacedClient).isNotNull();
assertThat(namespacedClient).isNotSameAs(podClient);
}
@@ -330,7 +330,7 @@ void inNamespace_returnsNewClient() {
@Test
void withName_returnsNewClient() {
ResourceClient namedClient = podClient.withName("my-pod");
-
+
assertThat(namedClient).isNotNull();
assertThat(namedClient).isNotSameAs(podClient);
}
@@ -364,7 +364,7 @@ void get_serverError_throwsApiException() {
.withStatus(500)
.withBody("{\"message\": \"Internal Server Error\"}")));
- assertThatThrownBy(() ->
+ assertThatThrownBy(() ->
podClient.inNamespace("default").withName("error-pod").get())
.isInstanceOf(ApiException.class);
}
@@ -378,7 +378,7 @@ void create_conflict_throwsApiException() {
.withStatus(409)
.withBody("{\"message\": \"AlreadyExists\"}")));
- assertThatThrownBy(() ->
+ assertThatThrownBy(() ->
podClient.inNamespace("default").create(pod))
.isInstanceOf(ApiException.class);
}
@@ -389,7 +389,7 @@ void create_conflict_throwsApiException() {
void createOrReplace_existingResource_updatesResource() throws ApiException {
V1Pod existingPod = createPod("existing-pod", "default");
existingPod.getMetadata().setResourceVersion("12345");
-
+
V1Pod updatedPod = createPod("existing-pod", "default");
updatedPod.getMetadata().setResourceVersion("12346");