diff --git a/util/src/main/java/io/kubernetes/client/util/Readiness.java b/util/src/main/java/io/kubernetes/client/util/Readiness.java new file mode 100644 index 0000000000..9117e1bc28 --- /dev/null +++ b/util/src/main/java/io/kubernetes/client/util/Readiness.java @@ -0,0 +1,506 @@ +/* +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.KubernetesObject; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1DeploymentCondition; +import io.kubernetes.client.openapi.models.V1DeploymentStatus; +import io.kubernetes.client.openapi.models.V1Node; +import io.kubernetes.client.openapi.models.V1NodeCondition; +import io.kubernetes.client.openapi.models.V1NodeStatus; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodCondition; +import io.kubernetes.client.openapi.models.V1PodStatus; +import io.kubernetes.client.openapi.models.V1ReplicaSet; +import io.kubernetes.client.openapi.models.V1ReplicaSetStatus; +import io.kubernetes.client.openapi.models.V1StatefulSet; +import io.kubernetes.client.openapi.models.V1StatefulSetStatus; +import io.kubernetes.client.openapi.models.V1DaemonSet; +import io.kubernetes.client.openapi.models.V1DaemonSetStatus; +import io.kubernetes.client.openapi.models.V1Job; +import io.kubernetes.client.openapi.models.V1JobCondition; +import io.kubernetes.client.openapi.models.V1JobStatus; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ReplicationController; +import io.kubernetes.client.openapi.models.V1ReplicationControllerStatus; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimStatus; +import io.kubernetes.client.openapi.models.V1Endpoints; + +import java.util.List; +import java.util.Objects; + +/** + * Utilities for checking the readiness of various Kubernetes resources. + * Provides fabric8-style readiness checks for common resource types. + * + *

Example usage: + *

{@code
+ * V1Pod pod = coreV1Api.readNamespacedPod("my-pod", "default").execute();
+ * if (Readiness.isPodReady(pod)) {
+ *     // Pod is ready
+ * }
+ *
+ * // Or use generic method:
+ * if (Readiness.isReady(pod)) {
+ *     // Resource is ready
+ * }
+ * }
+ */ +public class Readiness { + + private Readiness() { + // Utility class + } + + /** + * Checks if a Kubernetes resource is ready. + * Supports Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, Node, + * ReplicationController, PersistentVolumeClaim, Endpoints, and Service. + * + * @param resource the Kubernetes resource to check + * @return true if the resource is ready, false otherwise + */ + public static boolean isReady(KubernetesObject resource) { + if (resource == null) { + return false; + } + + if (resource instanceof V1Pod) { + return isPodReady((V1Pod) resource); + } else if (resource instanceof V1Deployment) { + return isDeploymentReady((V1Deployment) resource); + } else if (resource instanceof V1ReplicaSet) { + return isReplicaSetReady((V1ReplicaSet) resource); + } else if (resource instanceof V1StatefulSet) { + return isStatefulSetReady((V1StatefulSet) resource); + } else if (resource instanceof V1DaemonSet) { + return isDaemonSetReady((V1DaemonSet) resource); + } else if (resource instanceof V1Job) { + return isJobComplete((V1Job) resource); + } else if (resource instanceof V1Node) { + return isNodeReady((V1Node) resource); + } else if (resource instanceof V1ReplicationController) { + return isReplicationControllerReady((V1ReplicationController) resource); + } else if (resource instanceof V1PersistentVolumeClaim) { + return isPersistentVolumeClaimBound((V1PersistentVolumeClaim) resource); + } else if (resource instanceof V1Endpoints) { + return isEndpointsReady((V1Endpoints) resource); + } else if (resource instanceof V1Service) { + // Services are considered ready if they exist + return true; + } + + // For unknown types, consider them ready if they exist + return true; + } + + /** + * Checks if a Pod is ready. + * A Pod is considered ready if all containers are ready and the Pod + * has the "Ready" condition set to "True". + * + * @param pod the Pod to check + * @return true if the Pod is ready, false otherwise + */ + public static boolean isPodReady(V1Pod pod) { + if (pod == null) { + return false; + } + + V1PodStatus status = pod.getStatus(); + if (status == null) { + return false; + } + + // Check for Running phase + String phase = status.getPhase(); + if (!"Running".equals(phase)) { + return false; + } + + // Check Ready condition + List conditions = status.getConditions(); + if (conditions == null || conditions.isEmpty()) { + return false; + } + + return conditions.stream() + .filter(c -> "Ready".equals(c.getType())) + .findFirst() + .map(c -> "True".equals(c.getStatus())) + .orElse(false); + } + + /** + * Checks if a Deployment is ready. + * A Deployment is considered ready when the number of available replicas + * equals the desired number of replicas. + * + * @param deployment the Deployment to check + * @return true if the Deployment is ready, false otherwise + */ + public static boolean isDeploymentReady(V1Deployment deployment) { + if (deployment == null) { + return false; + } + + V1DeploymentStatus status = deployment.getStatus(); + if (status == null) { + return false; + } + + // Check Available condition + List conditions = status.getConditions(); + if (conditions != null) { + boolean available = conditions.stream() + .filter(c -> "Available".equals(c.getType())) + .anyMatch(c -> "True".equals(c.getStatus())); + if (!available) { + return false; + } + } + + Integer replicas = deployment.getSpec() != null ? deployment.getSpec().getReplicas() : null; + Integer readyReplicas = status.getReadyReplicas(); + Integer availableReplicas = status.getAvailableReplicas(); + + if (replicas == null) { + replicas = 1; // Default replicas is 1 + } + + return Objects.equals(replicas, readyReplicas) && + Objects.equals(replicas, availableReplicas); + } + + /** + * Checks if a ReplicaSet is ready. + * A ReplicaSet is considered ready when the number of ready replicas + * equals the desired number of replicas. + * + * @param replicaSet the ReplicaSet to check + * @return true if the ReplicaSet is ready, false otherwise + */ + public static boolean isReplicaSetReady(V1ReplicaSet replicaSet) { + if (replicaSet == null) { + return false; + } + + V1ReplicaSetStatus status = replicaSet.getStatus(); + if (status == null) { + return false; + } + + Integer replicas = replicaSet.getSpec() != null ? replicaSet.getSpec().getReplicas() : null; + Integer readyReplicas = status.getReadyReplicas(); + + if (replicas == null) { + replicas = 1; + } + if (readyReplicas == null) { + readyReplicas = 0; + } + + return replicas.equals(readyReplicas); + } + + /** + * Checks if a StatefulSet is ready. + * A StatefulSet is considered ready when the number of ready replicas + * equals the desired number of replicas. + * + * @param statefulSet the StatefulSet to check + * @return true if the StatefulSet is ready, false otherwise + */ + public static boolean isStatefulSetReady(V1StatefulSet statefulSet) { + if (statefulSet == null) { + return false; + } + + V1StatefulSetStatus status = statefulSet.getStatus(); + if (status == null) { + return false; + } + + Integer replicas = statefulSet.getSpec() != null ? statefulSet.getSpec().getReplicas() : null; + Integer readyReplicas = status.getReadyReplicas(); + + if (replicas == null) { + replicas = 1; + } + if (readyReplicas == null) { + readyReplicas = 0; + } + + return replicas.equals(readyReplicas); + } + + /** + * Checks if a DaemonSet is ready. + * A DaemonSet is considered ready when the number of ready nodes + * equals the desired number of scheduled nodes. + * + * @param daemonSet the DaemonSet to check + * @return true if the DaemonSet is ready, false otherwise + */ + public static boolean isDaemonSetReady(V1DaemonSet daemonSet) { + if (daemonSet == null) { + return false; + } + + V1DaemonSetStatus status = daemonSet.getStatus(); + if (status == null) { + return false; + } + + Integer desiredNumberScheduled = status.getDesiredNumberScheduled(); + Integer numberReady = status.getNumberReady(); + + if (desiredNumberScheduled == null) { + desiredNumberScheduled = 0; + } + if (numberReady == null) { + numberReady = 0; + } + + return desiredNumberScheduled > 0 && desiredNumberScheduled.equals(numberReady); + } + + /** + * Checks if a Job has completed successfully. + * A Job is considered complete when it has a "Complete" condition set to "True". + * + * @param job the Job to check + * @return true if the Job is complete, false otherwise + */ + public static boolean isJobComplete(V1Job job) { + if (job == null) { + return false; + } + + V1JobStatus status = job.getStatus(); + if (status == null) { + return false; + } + + List conditions = status.getConditions(); + if (conditions == null || conditions.isEmpty()) { + return false; + } + + return conditions.stream() + .filter(c -> "Complete".equals(c.getType())) + .anyMatch(c -> "True".equals(c.getStatus())); + } + + /** + * Checks if a Job has failed. + * + * @param job the Job to check + * @return true if the Job has failed, false otherwise + */ + public static boolean isJobFailed(V1Job job) { + if (job == null) { + return false; + } + + V1JobStatus status = job.getStatus(); + if (status == null) { + return false; + } + + List conditions = status.getConditions(); + if (conditions == null || conditions.isEmpty()) { + return false; + } + + return conditions.stream() + .filter(c -> "Failed".equals(c.getType())) + .anyMatch(c -> "True".equals(c.getStatus())); + } + + /** + * Checks if a Node is ready. + * A Node is considered ready when it has the "Ready" condition set to "True". + * + * @param node the Node to check + * @return true if the Node is ready, false otherwise + */ + public static boolean isNodeReady(V1Node node) { + if (node == null) { + return false; + } + + V1NodeStatus status = node.getStatus(); + if (status == null) { + return false; + } + + List conditions = status.getConditions(); + if (conditions == null || conditions.isEmpty()) { + return false; + } + + return conditions.stream() + .filter(c -> "Ready".equals(c.getType())) + .anyMatch(c -> "True".equals(c.getStatus())); + } + + /** + * Checks if a ReplicationController is ready. + * A ReplicationController is considered ready when the number of ready replicas + * equals the desired number of replicas. + * + * @param replicationController the ReplicationController to check + * @return true if the ReplicationController is ready, false otherwise + */ + public static boolean isReplicationControllerReady(V1ReplicationController replicationController) { + if (replicationController == null) { + return false; + } + + V1ReplicationControllerStatus status = replicationController.getStatus(); + if (status == null) { + return false; + } + + Integer replicas = replicationController.getSpec() != null + ? replicationController.getSpec().getReplicas() : null; + Integer readyReplicas = status.getReadyReplicas(); + + if (replicas == null) { + replicas = 1; + } + if (readyReplicas == null) { + readyReplicas = 0; + } + + return replicas.equals(readyReplicas); + } + + /** + * Checks if a PersistentVolumeClaim is bound. + * + * @param pvc the PersistentVolumeClaim to check + * @return true if the PVC is bound, false otherwise + */ + public static boolean isPersistentVolumeClaimBound(V1PersistentVolumeClaim pvc) { + if (pvc == null) { + return false; + } + + V1PersistentVolumeClaimStatus status = pvc.getStatus(); + if (status == null) { + return false; + } + + return "Bound".equals(status.getPhase()); + } + + /** + * Checks if Endpoints are ready (have at least one address). + * + * @param endpoints the Endpoints to check + * @return true if the Endpoints have at least one address, false otherwise + */ + public static boolean isEndpointsReady(V1Endpoints endpoints) { + if (endpoints == null) { + return false; + } + + if (endpoints.getSubsets() == null || endpoints.getSubsets().isEmpty()) { + return false; + } + + return endpoints.getSubsets().stream() + .anyMatch(subset -> subset.getAddresses() != null && !subset.getAddresses().isEmpty()); + } + + /** + * Checks if a Pod is in a terminal state (Succeeded or Failed). + * + * @param pod the Pod to check + * @return true if the Pod is in a terminal state, false otherwise + */ + public static boolean isPodTerminal(V1Pod pod) { + if (pod == null) { + return false; + } + + V1PodStatus status = pod.getStatus(); + if (status == null) { + return false; + } + + String phase = status.getPhase(); + return "Succeeded".equals(phase) || "Failed".equals(phase); + } + + /** + * Checks if a Pod has succeeded. + * + * @param pod the Pod to check + * @return true if the Pod has succeeded, false otherwise + */ + public static boolean isPodSucceeded(V1Pod pod) { + if (pod == null) { + return false; + } + + V1PodStatus status = pod.getStatus(); + if (status == null) { + return false; + } + + return "Succeeded".equals(status.getPhase()); + } + + /** + * Checks if a Pod has failed. + * + * @param pod the Pod to check + * @return true if the Pod has failed, false otherwise + */ + public static boolean isPodFailed(V1Pod pod) { + if (pod == null) { + return false; + } + + V1PodStatus status = pod.getStatus(); + if (status == null) { + return false; + } + + return "Failed".equals(status.getPhase()); + } + + /** + * Checks if a Pod is running. + * + * @param pod the Pod to check + * @return true if the Pod is running, false otherwise + */ + public static boolean isPodRunning(V1Pod pod) { + if (pod == null) { + return false; + } + + V1PodStatus status = pod.getStatus(); + if (status == null) { + return false; + } + + return "Running".equals(status.getPhase()); + } +} diff --git a/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java b/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java new file mode 100644 index 0000000000..e1683f0c44 --- /dev/null +++ b/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java @@ -0,0 +1,450 @@ +/* +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 static org.assertj.core.api.Assertions.assertThat; + +import io.kubernetes.client.openapi.models.V1DaemonSet; +import io.kubernetes.client.openapi.models.V1DaemonSetSpec; +import io.kubernetes.client.openapi.models.V1DaemonSetStatus; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1DeploymentCondition; +import io.kubernetes.client.openapi.models.V1DeploymentSpec; +import io.kubernetes.client.openapi.models.V1DeploymentStatus; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1Job; +import io.kubernetes.client.openapi.models.V1JobCondition; +import io.kubernetes.client.openapi.models.V1JobStatus; +import io.kubernetes.client.openapi.models.V1Node; +import io.kubernetes.client.openapi.models.V1NodeCondition; +import io.kubernetes.client.openapi.models.V1NodeStatus; +import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; +import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimStatus; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodCondition; +import io.kubernetes.client.openapi.models.V1PodStatus; +import io.kubernetes.client.openapi.models.V1ReplicaSet; +import io.kubernetes.client.openapi.models.V1ReplicaSetSpec; +import io.kubernetes.client.openapi.models.V1ReplicaSetStatus; +import io.kubernetes.client.openapi.models.V1ReplicationController; +import io.kubernetes.client.openapi.models.V1ReplicationControllerStatus; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1StatefulSet; +import io.kubernetes.client.openapi.models.V1StatefulSetSpec; +import io.kubernetes.client.openapi.models.V1StatefulSetStatus; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ReadinessTest { + + // ========== Pod Tests ========== + + @Test + void isPodReady_nullPod_returnsFalse() { + assertThat(Readiness.isPodReady(null)).isFalse(); + } + + @Test + void isPodReady_nullStatus_returnsFalse() { + V1Pod pod = new V1Pod().metadata(new V1ObjectMeta().name("test")); + assertThat(Readiness.isPodReady(pod)).isFalse(); + } + + @Test + void isPodReady_pendingPhase_returnsFalse() { + V1Pod pod = new V1Pod() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PodStatus().phase("Pending")); + assertThat(Readiness.isPodReady(pod)).isFalse(); + } + + @Test + void isPodReady_runningWithReadyConditionTrue_returnsTrue() { + V1Pod pod = new V1Pod() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PodStatus() + .phase("Running") + .conditions(List.of( + new V1PodCondition().type("Ready").status("True")))); + assertThat(Readiness.isPodReady(pod)).isTrue(); + } + + @Test + void isPodReady_runningWithReadyConditionFalse_returnsFalse() { + V1Pod pod = new V1Pod() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PodStatus() + .phase("Running") + .conditions(List.of( + new V1PodCondition().type("Ready").status("False")))); + assertThat(Readiness.isPodReady(pod)).isFalse(); + } + + @Test + void isPodReady_runningWithNoConditions_returnsFalse() { + V1Pod pod = new V1Pod() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PodStatus().phase("Running")); + assertThat(Readiness.isPodReady(pod)).isFalse(); + } + + // ========== Deployment Tests ========== + + @Test + void isDeploymentReady_nullDeployment_returnsFalse() { + assertThat(Readiness.isDeploymentReady(null)).isFalse(); + } + + @Test + void isDeploymentReady_nullStatus_returnsFalse() { + V1Deployment deployment = new V1Deployment() + .metadata(new V1ObjectMeta().name("test")); + assertThat(Readiness.isDeploymentReady(deployment)).isFalse(); + } + + @Test + void isDeploymentReady_availableWithCorrectReplicas_returnsTrue() { + V1Deployment deployment = new V1Deployment() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1DeploymentSpec().replicas(3)) + .status(new V1DeploymentStatus() + .replicas(3) + .readyReplicas(3) + .availableReplicas(3) + .conditions(List.of( + new V1DeploymentCondition() + .type("Available") + .status("True")))); + assertThat(Readiness.isDeploymentReady(deployment)).isTrue(); + } + + @Test + void isDeploymentReady_notAvailable_returnsFalse() { + V1Deployment deployment = new V1Deployment() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1DeploymentSpec().replicas(3)) + .status(new V1DeploymentStatus() + .replicas(3) + .readyReplicas(1) + .availableReplicas(1) + .conditions(List.of( + new V1DeploymentCondition() + .type("Available") + .status("False")))); + assertThat(Readiness.isDeploymentReady(deployment)).isFalse(); + } + + @Test + void isDeploymentReady_fewerReadyReplicas_returnsFalse() { + V1Deployment deployment = new V1Deployment() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1DeploymentSpec().replicas(3)) + .status(new V1DeploymentStatus() + .replicas(3) + .readyReplicas(2) + .availableReplicas(3) + .conditions(List.of( + new V1DeploymentCondition() + .type("Available") + .status("True")))); + assertThat(Readiness.isDeploymentReady(deployment)).isFalse(); + } + + // ========== StatefulSet Tests ========== + + @Test + void isStatefulSetReady_nullStatefulSet_returnsFalse() { + assertThat(Readiness.isStatefulSetReady(null)).isFalse(); + } + + @Test + void isStatefulSetReady_allReplicasReady_returnsTrue() { + V1StatefulSet statefulSet = new V1StatefulSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1StatefulSetSpec().replicas(3)) + .status(new V1StatefulSetStatus() + .replicas(3) + .readyReplicas(3)); + assertThat(Readiness.isStatefulSetReady(statefulSet)).isTrue(); + } + + @Test + void isStatefulSetReady_notAllReplicasReady_returnsFalse() { + V1StatefulSet statefulSet = new V1StatefulSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1StatefulSetSpec().replicas(3)) + .status(new V1StatefulSetStatus() + .replicas(3) + .readyReplicas(2)); + assertThat(Readiness.isStatefulSetReady(statefulSet)).isFalse(); + } + + // ========== ReplicaSet Tests ========== + + @Test + void isReplicaSetReady_nullReplicaSet_returnsFalse() { + assertThat(Readiness.isReplicaSetReady(null)).isFalse(); + } + + @Test + void isReplicaSetReady_allReplicasReady_returnsTrue() { + V1ReplicaSet replicaSet = new V1ReplicaSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1ReplicaSetSpec().replicas(3)) + .status(new V1ReplicaSetStatus() + .replicas(3) + .readyReplicas(3)); + assertThat(Readiness.isReplicaSetReady(replicaSet)).isTrue(); + } + + @Test + void isReplicaSetReady_notAllReplicasReady_returnsFalse() { + V1ReplicaSet replicaSet = new V1ReplicaSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1ReplicaSetSpec().replicas(3)) + .status(new V1ReplicaSetStatus() + .replicas(3) + .readyReplicas(1)); + assertThat(Readiness.isReplicaSetReady(replicaSet)).isFalse(); + } + + // ========== DaemonSet Tests ========== + + @Test + void isDaemonSetReady_nullDaemonSet_returnsFalse() { + assertThat(Readiness.isDaemonSetReady(null)).isFalse(); + } + + @Test + void isDaemonSetReady_allNodesScheduled_returnsTrue() { + V1DaemonSet daemonSet = new V1DaemonSet() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1DaemonSetStatus() + .desiredNumberScheduled(5) + .numberReady(5)); + assertThat(Readiness.isDaemonSetReady(daemonSet)).isTrue(); + } + + @Test + void isDaemonSetReady_notAllNodesReady_returnsFalse() { + V1DaemonSet daemonSet = new V1DaemonSet() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1DaemonSetStatus() + .desiredNumberScheduled(5) + .numberReady(3)); + assertThat(Readiness.isDaemonSetReady(daemonSet)).isFalse(); + } + + // ========== Job Tests ========== + + @Test + void isJobComplete_nullJob_returnsFalse() { + assertThat(Readiness.isJobComplete(null)).isFalse(); + } + + @Test + void isJobComplete_withCompleteCondition_returnsTrue() { + V1Job job = new V1Job() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1JobStatus() + .conditions(List.of( + new V1JobCondition() + .type("Complete") + .status("True")))); + assertThat(Readiness.isJobComplete(job)).isTrue(); + } + + @Test + void isJobComplete_withFailedCondition_returnsFalse() { + V1Job job = new V1Job() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1JobStatus() + .conditions(List.of( + new V1JobCondition() + .type("Failed") + .status("True")))); + assertThat(Readiness.isJobComplete(job)).isFalse(); + } + + @Test + void isJobComplete_noConditions_returnsFalse() { + V1Job job = new V1Job() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1JobStatus()); + assertThat(Readiness.isJobComplete(job)).isFalse(); + } + + // ========== Node Tests ========== + + @Test + void isNodeReady_nullNode_returnsFalse() { + assertThat(Readiness.isNodeReady(null)).isFalse(); + } + + @Test + void isNodeReady_withReadyConditionTrue_returnsTrue() { + V1Node node = new V1Node() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1NodeStatus() + .conditions(List.of( + new V1NodeCondition() + .type("Ready") + .status("True")))); + assertThat(Readiness.isNodeReady(node)).isTrue(); + } + + @Test + void isNodeReady_withReadyConditionFalse_returnsFalse() { + V1Node node = new V1Node() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1NodeStatus() + .conditions(List.of( + new V1NodeCondition() + .type("Ready") + .status("False")))); + assertThat(Readiness.isNodeReady(node)).isFalse(); + } + + // ========== ReplicationController Tests ========== + + @Test + void isReplicationControllerReady_nullController_returnsFalse() { + assertThat(Readiness.isReplicationControllerReady(null)).isFalse(); + } + + @Test + void isReplicationControllerReady_allReplicasReady_returnsTrue() { + V1ReplicationController rc = new V1ReplicationController() + .metadata(new V1ObjectMeta().name("test")) + .spec(new io.kubernetes.client.openapi.models.V1ReplicationControllerSpec().replicas(3)) + .status(new V1ReplicationControllerStatus() + .replicas(3) + .readyReplicas(3)); + assertThat(Readiness.isReplicationControllerReady(rc)).isTrue(); + } + + // ========== PersistentVolumeClaim Tests ========== + + @Test + void isPersistentVolumeClaimBound_nullPvc_returnsFalse() { + assertThat(Readiness.isPersistentVolumeClaimBound(null)).isFalse(); + } + + @Test + void isPersistentVolumeClaimBound_boundPhase_returnsTrue() { + V1PersistentVolumeClaim pvc = new V1PersistentVolumeClaim() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PersistentVolumeClaimStatus().phase("Bound")); + assertThat(Readiness.isPersistentVolumeClaimBound(pvc)).isTrue(); + } + + @Test + void isPersistentVolumeClaimBound_pendingPhase_returnsFalse() { + V1PersistentVolumeClaim pvc = new V1PersistentVolumeClaim() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PersistentVolumeClaimStatus().phase("Pending")); + assertThat(Readiness.isPersistentVolumeClaimBound(pvc)).isFalse(); + } + + // ========== Endpoints Tests ========== + + @Test + void isEndpointsReady_nullEndpoints_returnsFalse() { + assertThat(Readiness.isEndpointsReady(null)).isFalse(); + } + + @Test + void isEndpointsReady_withAddresses_returnsTrue() { + V1Endpoints endpoints = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test")) + .subsets(List.of( + new V1EndpointSubset() + .addresses(List.of( + new V1EndpointAddress().ip("10.0.0.1"))))); + assertThat(Readiness.isEndpointsReady(endpoints)).isTrue(); + } + + @Test + void isEndpointsReady_noSubsets_returnsFalse() { + V1Endpoints endpoints = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test")); + assertThat(Readiness.isEndpointsReady(endpoints)).isFalse(); + } + + @Test + void isEndpointsReady_emptySubsets_returnsFalse() { + V1Endpoints endpoints = new V1Endpoints() + .metadata(new V1ObjectMeta().name("test")) + .subsets(Collections.emptyList()); + assertThat(Readiness.isEndpointsReady(endpoints)).isFalse(); + } + + // ========== Service Tests ========== + + @Test + void isReady_service_returnsTrue() { + V1Service service = new V1Service() + .metadata(new V1ObjectMeta().name("test")); + assertThat(Readiness.isReady(service)).isTrue(); + } + + // ========== Generic isReady Tests ========== + + @Test + void isReady_nullResource_returnsFalse() { + assertThat(Readiness.isReady(null)).isFalse(); + } + + @Test + void isReady_delegatesToCorrectMethod_forPod() { + V1Pod pod = new V1Pod() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1PodStatus() + .phase("Running") + .conditions(List.of( + new V1PodCondition().type("Ready").status("True")))); + assertThat(Readiness.isReady(pod)).isTrue(); + } + + @Test + void isReady_delegatesToCorrectMethod_forDeployment() { + V1Deployment deployment = new V1Deployment() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1DeploymentSpec().replicas(1)) + .status(new V1DeploymentStatus() + .replicas(1) + .readyReplicas(1) + .availableReplicas(1) + .conditions(List.of( + new V1DeploymentCondition() + .type("Available") + .status("True")))); + assertThat(Readiness.isReady(deployment)).isTrue(); + } + + @Test + void isReady_delegatesToCorrectMethod_forJob() { + V1Job job = new V1Job() + .metadata(new V1ObjectMeta().name("test")) + .status(new V1JobStatus() + .conditions(List.of( + new V1JobCondition() + .type("Complete") + .status("True")))); + assertThat(Readiness.isReady(job)).isTrue(); + } +}