diff --git a/util/src/main/java/io/kubernetes/client/util/Readiness.java b/util/src/main/java/io/kubernetes/client/util/Readiness.java index 715320574e..ec8eafa476 100644 --- a/util/src/main/java/io/kubernetes/client/util/Readiness.java +++ b/util/src/main/java/io/kubernetes/client/util/Readiness.java @@ -206,13 +206,15 @@ public static boolean isReplicaSetReady(V1ReplicaSet replicaSet) { Integer readyReplicas = status.getReadyReplicas(); if (replicas == null) { - replicas = 1; - } - if (readyReplicas == null) { - readyReplicas = 0; + return false; } + // readyReplicas is `omitempty` in the real API (a plain int, not a pointer), so the + // API server omits it from the JSON whenever it's genuinely 0 - not just when it + // hasn't been reported yet. A null here means "zero", matching client-go's own + // comparison (*(rs.Spec.Replicas) == rs.Status.ReadyReplicas), not "unknown". + int effectiveReadyReplicas = readyReplicas == null ? 0 : readyReplicas; - return replicas.equals(readyReplicas); + return replicas.equals(effectiveReadyReplicas); } /** @@ -380,13 +382,15 @@ public static boolean isReplicationControllerReady(V1ReplicationController repli Integer readyReplicas = status.getReadyReplicas(); if (replicas == null) { - replicas = 1; - } - if (readyReplicas == null) { - readyReplicas = 0; + return false; } + // readyReplicas is `omitempty` in the real API (a plain int, not a pointer), so the + // API server omits it from the JSON whenever it's genuinely 0 - not just when it + // hasn't been reported yet. A null here means "zero", matching client-go's own + // semantics, not "unknown". + int effectiveReadyReplicas = readyReplicas == null ? 0 : readyReplicas; - return replicas.equals(readyReplicas); + return replicas.equals(effectiveReadyReplicas); } /** diff --git a/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java b/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java index e1683f0c44..52d2cfdccb 100644 --- a/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java +++ b/util/src/test/java/io/kubernetes/client/util/ReadinessTest.java @@ -222,6 +222,41 @@ void isReplicaSetReady_notAllReplicasReady_returnsFalse() { assertThat(Readiness.isReplicaSetReady(replicaSet)).isFalse(); } + @Test + void isReplicaSetReady_nullSpecReplicas_returnsFalse() { + // Regression test: a null spec.replicas must not be silently treated as "1". + V1ReplicaSet replicaSet = new V1ReplicaSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1ReplicaSetSpec()) + .status(new V1ReplicaSetStatus().readyReplicas(1)); + assertThat(Readiness.isReplicaSetReady(replicaSet)).isFalse(); + } + + @Test + void isReplicaSetReady_zeroReplicasWithNullReadyReplicas_returnsTrue() { + // readyReplicas is `omitempty` on a plain int in the real API, so the API server + // omits it from the JSON whenever it's genuinely 0 - not just when it hasn't been + // reported yet. A null readyReplicas here should be treated as 0, matching + // client-go's own WaitForReadyReplicaSet check (spec.replicas == status.readyReplicas), + // which considers a zero-replica ReplicaSet ready once its status is all-zero. + V1ReplicaSet replicaSet = new V1ReplicaSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1ReplicaSetSpec().replicas(0)) + .status(new V1ReplicaSetStatus()); + assertThat(Readiness.isReplicaSetReady(replicaSet)).isTrue(); + } + + @Test + void isReplicaSetReady_nonZeroReplicasWithNullReadyReplicas_returnsFalse() { + // A null readyReplicas is treated as 0 (see above), so a non-zero desired replica + // count still correctly reports not-ready when readyReplicas is null. + V1ReplicaSet replicaSet = new V1ReplicaSet() + .metadata(new V1ObjectMeta().name("test")) + .spec(new V1ReplicaSetSpec().replicas(3)) + .status(new V1ReplicaSetStatus()); + assertThat(Readiness.isReplicaSetReady(replicaSet)).isFalse(); + } + // ========== DaemonSet Tests ========== @Test @@ -337,6 +372,39 @@ void isReplicationControllerReady_allReplicasReady_returnsTrue() { assertThat(Readiness.isReplicationControllerReady(rc)).isTrue(); } + @Test + void isReplicationControllerReady_nullSpecReplicas_returnsFalse() { + // Regression test: a null spec.replicas must not be silently treated as "1". + V1ReplicationController rc = new V1ReplicationController() + .metadata(new V1ObjectMeta().name("test")) + .spec(new io.kubernetes.client.openapi.models.V1ReplicationControllerSpec()) + .status(new V1ReplicationControllerStatus().readyReplicas(1)); + assertThat(Readiness.isReplicationControllerReady(rc)).isFalse(); + } + + @Test + void isReplicationControllerReady_zeroReplicasWithNullReadyReplicas_returnsTrue() { + // readyReplicas is `omitempty` on a plain int in the real API, so it's omitted from + // the JSON whenever it's genuinely 0, not just when unreported. A null readyReplicas + // should be treated as 0, matching client-go's semantics for a zero-replica resource. + V1ReplicationController rc = new V1ReplicationController() + .metadata(new V1ObjectMeta().name("test")) + .spec(new io.kubernetes.client.openapi.models.V1ReplicationControllerSpec().replicas(0)) + .status(new V1ReplicationControllerStatus()); + assertThat(Readiness.isReplicationControllerReady(rc)).isTrue(); + } + + @Test + void isReplicationControllerReady_nonZeroReplicasWithNullReadyReplicas_returnsFalse() { + // A null readyReplicas is treated as 0 (see above), so a non-zero desired replica + // count still correctly reports not-ready when readyReplicas is null. + V1ReplicationController rc = new V1ReplicationController() + .metadata(new V1ObjectMeta().name("test")) + .spec(new io.kubernetes.client.openapi.models.V1ReplicationControllerSpec().replicas(3)) + .status(new V1ReplicationControllerStatus()); + assertThat(Readiness.isReplicationControllerReady(rc)).isFalse(); + } + // ========== PersistentVolumeClaim Tests ========== @Test @@ -447,4 +515,4 @@ void isReady_delegatesToCorrectMethod_forJob() { .status("True")))); assertThat(Readiness.isReady(job)).isTrue(); } -} +} \ No newline at end of file