diff --git a/pkg/api/resource_helpers.go b/pkg/api/resource_helpers.go index 65c1591caac..e926094bb6f 100644 --- a/pkg/api/resource_helpers.go +++ b/pkg/api/resource_helpers.go @@ -17,6 +17,8 @@ limitations under the License. package api import ( + "time" + "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/api/unversioned" ) @@ -74,6 +76,24 @@ func GetExistingContainerStatus(statuses []ContainerStatus, name string) Contain return ContainerStatus{} } +// IsPodAvailable returns true if a pod is available; false otherwise. +// Precondition for an available pod is that it must be ready. On top +// of that, there are two cases when a pod can be considered available: +// 1. minReadySeconds == 0, or +// 2. LastTransitionTime (is set) + minReadySeconds < current time +func IsPodAvailable(pod *Pod, minReadySeconds int32, now unversioned.Time) bool { + if !IsPodReady(pod) { + return false + } + + c := GetPodReadyCondition(pod.Status) + minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second + if minReadySeconds == 0 || !c.LastTransitionTime.IsZero() && c.LastTransitionTime.Add(minReadySecondsDuration).Before(now.Time) { + return true + } + return false +} + // IsPodReady returns true if a pod is ready; false otherwise. func IsPodReady(pod *Pod) bool { return IsPodReadyConditionTrue(pod.Status) diff --git a/pkg/api/resource_helpers_test.go b/pkg/api/resource_helpers_test.go index 4c5f6a625ab..53803cfe1f2 100644 --- a/pkg/api/resource_helpers_test.go +++ b/pkg/api/resource_helpers_test.go @@ -18,8 +18,10 @@ package api import ( "testing" + "time" "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/api/unversioned" ) func TestResourceHelpers(t *testing.T) { @@ -61,3 +63,58 @@ func TestDefaultResourceHelpers(t *testing.T) { t.Errorf("expected %v, actual %v", resource.BinarySI, resourceList.Memory().Format) } } + +func newPod(now unversioned.Time, ready bool, beforeSec int) *Pod { + conditionStatus := ConditionFalse + if ready { + conditionStatus = ConditionTrue + } + return &Pod{ + Status: PodStatus{ + Conditions: []PodCondition{ + { + Type: PodReady, + LastTransitionTime: unversioned.NewTime(now.Time.Add(-1 * time.Duration(beforeSec) * time.Second)), + Status: conditionStatus, + }, + }, + }, + } +} + +func TestIsPodAvailable(t *testing.T) { + now := unversioned.Now() + tests := []struct { + pod *Pod + minReadySeconds int32 + expected bool + }{ + { + pod: newPod(now, false, 0), + minReadySeconds: 0, + expected: false, + }, + { + pod: newPod(now, true, 0), + minReadySeconds: 1, + expected: false, + }, + { + pod: newPod(now, true, 0), + minReadySeconds: 0, + expected: true, + }, + { + pod: newPod(now, true, 51), + minReadySeconds: 50, + expected: true, + }, + } + + for i, test := range tests { + isAvailable := IsPodAvailable(test.pod, test.minReadySeconds, now) + if isAvailable != test.expected { + t.Errorf("[tc #%d] expected available pod: %t, got: %t", i, test.expected, isAvailable) + } + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 7d29c8b52d7..7f4f01aedd1 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1717,6 +1717,11 @@ type ReplicationControllerSpec struct { // Replicas is the number of desired replicas. Replicas int32 `json:"replicas"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + MinReadySeconds int32 `json:"minReadySeconds,omitempty"` + // Selector is a label query over pods that should match the Replicas count. Selector map[string]string `json:"selector"` @@ -1743,6 +1748,9 @@ type ReplicationControllerStatus struct { // The number of ready replicas for this replication controller. ReadyReplicas int32 `json:"readyReplicas,omitempty"` + // The number of available replicas (ready for at least minReadySeconds) for this replication controller. + AvailableReplicas int32 `json:"availableReplicas,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. ObservedGeneration int64 `json:"observedGeneration,omitempty"` } diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 7b37b610594..fd181958d2c 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -2023,6 +2023,11 @@ type ReplicationControllerSpec struct { // More info: http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + MinReadySeconds int32 `json:"minReadySeconds,omitempty"` + // Selector is a label query over pods that should match the Replicas count. // If Selector is empty, it is defaulted to the labels present on the Pod template. // Label keys and values that must match in order to be controlled by this replication @@ -2054,6 +2059,9 @@ type ReplicationControllerStatus struct { // The number of ready replicas for this replication controller. ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,4,opt,name=readyReplicas"` + // The number of available replicas (ready for at least minReadySeconds) for this replication controller. + AvailableReplicas int32 `json:"availableReplicas,omitempty"` + // ObservedGeneration reflects the generation of the most recently observed replication controller. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` } diff --git a/pkg/api/v1/zz_generated.conversion.go b/pkg/api/v1/zz_generated.conversion.go index ce5892b25be..a38a6c6041d 100644 --- a/pkg/api/v1/zz_generated.conversion.go +++ b/pkg/api/v1/zz_generated.conversion.go @@ -5655,6 +5655,7 @@ func autoConvert_v1_ReplicationControllerSpec_To_api_ReplicationControllerSpec(i if err := api.Convert_Pointer_int32_To_int32(&in.Replicas, &out.Replicas, s); err != nil { return err } + out.MinReadySeconds = in.MinReadySeconds out.Selector = in.Selector if in.Template != nil { in, out := &in.Template, &out.Template @@ -5672,6 +5673,7 @@ func autoConvert_api_ReplicationControllerSpec_To_v1_ReplicationControllerSpec(i if err := api.Convert_int32_To_Pointer_int32(&in.Replicas, &out.Replicas, s); err != nil { return err } + out.MinReadySeconds = in.MinReadySeconds out.Selector = in.Selector if in.Template != nil { in, out := &in.Template, &out.Template @@ -5689,6 +5691,7 @@ func autoConvert_v1_ReplicationControllerStatus_To_api_ReplicationControllerStat out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } @@ -5701,6 +5704,7 @@ func autoConvert_api_ReplicationControllerStatus_To_v1_ReplicationControllerStat out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index 730a3d68357..8ba4c86ccf7 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -2942,6 +2942,7 @@ func DeepCopy_v1_ReplicationControllerSpec(in interface{}, out interface{}, c *c } else { out.Replicas = nil } + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = make(map[string]string) @@ -2971,6 +2972,7 @@ func DeepCopy_v1_ReplicationControllerStatus(in interface{}, out interface{}, c out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 669ad2689f0..0b3d433eb68 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -2606,6 +2606,8 @@ func ValidateReplicationControllerStatusUpdate(controller, oldController *api.Re statusPath := field.NewPath("status") allErrs = append(allErrs, ValidateNonnegativeField(int64(controller.Status.Replicas), statusPath.Child("replicas"))...) allErrs = append(allErrs, ValidateNonnegativeField(int64(controller.Status.FullyLabeledReplicas), statusPath.Child("fullyLabeledReplicas"))...) + allErrs = append(allErrs, ValidateNonnegativeField(int64(controller.Status.ReadyReplicas), statusPath.Child("readyReplicas"))...) + allErrs = append(allErrs, ValidateNonnegativeField(int64(controller.Status.AvailableReplicas), statusPath.Child("availableReplicas"))...) allErrs = append(allErrs, ValidateNonnegativeField(int64(controller.Status.ObservedGeneration), statusPath.Child("observedGeneration"))...) return allErrs } @@ -2649,6 +2651,7 @@ func ValidatePodTemplateSpecForRC(template *api.PodTemplateSpec, selectorMap map // ValidateReplicationControllerSpec tests if required fields in the replication controller spec are set. func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, fldPath.Child("selector"))...) allErrs = append(allErrs, ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, fldPath.Child("template"))...) diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 333f9888be5..a78498cc331 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -2994,6 +2994,7 @@ func DeepCopy_api_ReplicationControllerSpec(in interface{}, out interface{}, c * in := in.(*ReplicationControllerSpec) out := out.(*ReplicationControllerSpec) out.Replicas = in.Replicas + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = make(map[string]string) @@ -3023,6 +3024,7 @@ func DeepCopy_api_ReplicationControllerStatus(in interface{}, out interface{}, c out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 988485fcc08..890123d1d35 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -610,6 +610,11 @@ type ReplicaSetSpec struct { // Replicas is the number of desired replicas. Replicas int32 `json:"replicas"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + MinReadySeconds int32 `json:"minReadySeconds,omitempty"` + // Selector is a label query over pods that should match the replica count. // Must match in order to be controlled. // If empty, defaulted to labels on pod template. @@ -632,6 +637,9 @@ type ReplicaSetStatus struct { // The number of ready replicas for this replica set. ReadyReplicas int32 `json:"readyReplicas,omitempty"` + // The number of available replicas (ready for at least minReadySeconds) for this replica set. + AvailableReplicas int32 `json:"availableReplicas,omitempty"` + // ObservedGeneration is the most recent generation observed by the controller. ObservedGeneration int64 `json:"observedGeneration,omitempty"` } diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index f3f10820e16..5ffcc460b1b 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -887,6 +887,11 @@ type ReplicaSetSpec struct { // More info: http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#what-is-a-replication-controller Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + MinReadySeconds int32 `json:"minReadySeconds,omitempty"` + // Selector is a label query over pods that should match the replica count. // If the selector is empty, it is defaulted to the labels present on the pod template. // Label keys and values that must match in order to be controlled by this replica set. @@ -911,6 +916,9 @@ type ReplicaSetStatus struct { // The number of ready replicas for this replica set. ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,4,opt,name=readyReplicas"` + // The number of available replicas (ready for at least minReadySeconds) for this replica set. + AvailableReplicas int32 `json:"availableReplicas,omitempty"` + // ObservedGeneration reflects the generation of the most recently observed ReplicaSet. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` } diff --git a/pkg/apis/extensions/v1beta1/zz_generated.conversion.go b/pkg/apis/extensions/v1beta1/zz_generated.conversion.go index 22a50b3c69e..db80bf6f9e9 100644 --- a/pkg/apis/extensions/v1beta1/zz_generated.conversion.go +++ b/pkg/apis/extensions/v1beta1/zz_generated.conversion.go @@ -2240,6 +2240,7 @@ func autoConvert_v1beta1_ReplicaSetSpec_To_extensions_ReplicaSetSpec(in *Replica if err := api.Convert_Pointer_int32_To_int32(&in.Replicas, &out.Replicas, s); err != nil { return err } + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = new(unversioned.LabelSelector) @@ -2259,6 +2260,7 @@ func autoConvert_extensions_ReplicaSetSpec_To_v1beta1_ReplicaSetSpec(in *extensi if err := api.Convert_int32_To_Pointer_int32(&in.Replicas, &out.Replicas, s); err != nil { return err } + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = new(LabelSelector) @@ -2278,6 +2280,7 @@ func autoConvert_v1beta1_ReplicaSetStatus_To_extensions_ReplicaSetStatus(in *Rep out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } @@ -2290,6 +2293,7 @@ func autoConvert_extensions_ReplicaSetStatus_To_v1beta1_ReplicaSetStatus(in *ext out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/apis/extensions/v1beta1/zz_generated.deepcopy.go b/pkg/apis/extensions/v1beta1/zz_generated.deepcopy.go index 7d1539ed6c4..14051e4d38f 100644 --- a/pkg/apis/extensions/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/extensions/v1beta1/zz_generated.deepcopy.go @@ -1191,6 +1191,7 @@ func DeepCopy_v1beta1_ReplicaSetSpec(in interface{}, out interface{}, c *convers } else { out.Replicas = nil } + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = new(LabelSelector) @@ -1214,6 +1215,7 @@ func DeepCopy_v1beta1_ReplicaSetStatus(in interface{}, out interface{}, c *conve out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 61fda57c0de..da744174a6b 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -491,6 +491,8 @@ func ValidateReplicaSetStatusUpdate(rs, oldRs *extensions.ReplicaSet) field.Erro allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(rs.Status.Replicas), field.NewPath("status", "replicas"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(rs.Status.FullyLabeledReplicas), field.NewPath("status", "fullyLabeledReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(rs.Status.ReadyReplicas), field.NewPath("status", "readyReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(rs.Status.AvailableReplicas), field.NewPath("status", "availableReplicas"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(rs.Status.ObservedGeneration), field.NewPath("status", "observedGeneration"))...) return allErrs } @@ -500,6 +502,7 @@ func ValidateReplicaSetSpec(spec *extensions.ReplicaSetSpec, fldPath *field.Path allErrs := field.ErrorList{} allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) if spec.Selector == nil { allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) diff --git a/pkg/apis/extensions/zz_generated.deepcopy.go b/pkg/apis/extensions/zz_generated.deepcopy.go index 89ff863f6ea..8a9e4d988e8 100644 --- a/pkg/apis/extensions/zz_generated.deepcopy.go +++ b/pkg/apis/extensions/zz_generated.deepcopy.go @@ -846,6 +846,7 @@ func DeepCopy_extensions_ReplicaSetSpec(in interface{}, out interface{}, c *conv in := in.(*ReplicaSetSpec) out := out.(*ReplicaSetSpec) out.Replicas = in.Replicas + out.MinReadySeconds = in.MinReadySeconds if in.Selector != nil { in, out := &in.Selector, &out.Selector *out = new(unversioned.LabelSelector) @@ -869,6 +870,7 @@ func DeepCopy_extensions_ReplicaSetStatus(in interface{}, out interface{}, c *co out.Replicas = in.Replicas out.FullyLabeledReplicas = in.FullyLabeledReplicas out.ReadyReplicas = in.ReadyReplicas + out.AvailableReplicas = in.AvailableReplicas out.ObservedGeneration = in.ObservedGeneration return nil } diff --git a/pkg/controller/deployment/util/deployment_util.go b/pkg/controller/deployment/util/deployment_util.go index 8a92033728e..ed62f44158e 100644 --- a/pkg/controller/deployment/util/deployment_util.go +++ b/pkg/controller/deployment/util/deployment_util.go @@ -640,6 +640,8 @@ func countAvailablePods(pods []api.Pod, minReadySeconds int32) int32 { } // IsPodAvailable return true if the pod is available. +// TODO: Remove this once we start using replica set status for calculating available pods +// for a deployment. func IsPodAvailable(pod *api.Pod, minReadySeconds int32, now time.Time) bool { if !controller.IsPodActive(pod) { return false diff --git a/pkg/controller/replicaset/replica_set.go b/pkg/controller/replicaset/replica_set.go index 8dd63f726c3..825e05384e6 100644 --- a/pkg/controller/replicaset/replica_set.go +++ b/pkg/controller/replicaset/replica_set.go @@ -658,6 +658,7 @@ func (rsc *ReplicaSetController) syncReplicaSet(key string) error { // part of the filteredPods. fullyLabeledReplicasCount := 0 readyReplicasCount := 0 + availableReplicasCount := 0 templateLabel := labels.Set(rs.Spec.Template.Labels).AsSelectorPreValidated() for _, pod := range filteredPods { if templateLabel.Matches(labels.Set(pod.Labels)) { @@ -665,11 +666,21 @@ func (rsc *ReplicaSetController) syncReplicaSet(key string) error { } if api.IsPodReady(pod) { readyReplicasCount++ + if api.IsPodAvailable(pod, rs.Spec.MinReadySeconds, unversioned.Now()) { + availableReplicasCount++ + } } } // Always updates status as pods come up or die. - if err := updateReplicaCount(rsc.kubeClient.Extensions().ReplicaSets(rs.Namespace), rs, len(filteredPods), fullyLabeledReplicasCount, readyReplicasCount); err != nil { + if err := updateReplicaCount( + rsc.kubeClient.Extensions().ReplicaSets(rs.Namespace), + rs, + len(filteredPods), + fullyLabeledReplicasCount, + readyReplicasCount, + availableReplicasCount, + ); err != nil { // Multiple things could lead to this update failing. Requeuing the replica set ensures // we retry with some fairness. glog.V(2).Infof("Failed to update replica count for controller %v/%v; requeuing; error: %v", rs.Namespace, rs.Name, err) diff --git a/pkg/controller/replicaset/replica_set_test.go b/pkg/controller/replicaset/replica_set_test.go index fd7efd6cf30..0ff69064131 100644 --- a/pkg/controller/replicaset/replica_set_test.go +++ b/pkg/controller/replicaset/replica_set_test.go @@ -97,10 +97,14 @@ func newReplicaSet(replicas int, selectorMap map[string]string) *extensions.Repl } // create a pod with the given phase for the given rs (same selectors and namespace) -func newPod(name string, rs *extensions.ReplicaSet, status api.PodPhase) *api.Pod { +func newPod(name string, rs *extensions.ReplicaSet, status api.PodPhase, lastTransitionTime *unversioned.Time) *api.Pod { var conditions []api.PodCondition if status == api.PodRunning { - conditions = append(conditions, api.PodCondition{Type: api.PodReady, Status: api.ConditionTrue}) + condition := api.PodCondition{Type: api.PodReady, Status: api.ConditionTrue} + if lastTransitionTime != nil { + condition.LastTransitionTime = *lastTransitionTime + } + conditions = append(conditions, condition) } return &api.Pod{ ObjectMeta: api.ObjectMeta{ @@ -118,7 +122,7 @@ func newPodList(store cache.Store, count int, status api.PodPhase, labelMap map[ var trueVar = true controllerReference := api.OwnerReference{UID: rs.UID, APIVersion: "v1beta1", Kind: "ReplicaSet", Name: rs.Name, Controller: &trueVar} for i := 0; i < count; i++ { - pod := newPod(fmt.Sprintf("%s%d", name, i), rs, status) + pod := newPod(fmt.Sprintf("%s%d", name, i), rs, status, nil) pod.ObjectMeta.Labels = labelMap pod.OwnerReferences = []api.OwnerReference{controllerReference} if store != nil { @@ -253,7 +257,7 @@ func TestStatusUpdatesWithoutReplicasChange(t *testing.T) { labelMap := map[string]string{"foo": "bar"} rs := newReplicaSet(activePods, labelMap) manager.rsStore.Store.Add(rs) - rs.Status = extensions.ReplicaSetStatus{Replicas: int32(activePods), ReadyReplicas: int32(activePods)} + rs.Status = extensions.ReplicaSetStatus{Replicas: int32(activePods), ReadyReplicas: int32(activePods), AvailableReplicas: int32(activePods)} newPodList(manager.podStore.Indexer, activePods, api.PodRunning, labelMap, rs, "pod") fakePodControl := controller.FakePodControl{} @@ -298,7 +302,7 @@ func TestControllerUpdateReplicas(t *testing.T) { rs := newReplicaSet(5, labelMap) rs.Spec.Template.Labels = extraLabelMap manager.rsStore.Store.Add(rs) - rs.Status = extensions.ReplicaSetStatus{Replicas: 2, FullyLabeledReplicas: 6, ReadyReplicas: 2, ObservedGeneration: 0} + rs.Status = extensions.ReplicaSetStatus{Replicas: 2, FullyLabeledReplicas: 6, ReadyReplicas: 2, AvailableReplicas: 2, ObservedGeneration: 0} rs.Generation = 1 newPodList(manager.podStore.Indexer, 2, api.PodRunning, labelMap, rs, "pod") newPodList(manager.podStore.Indexer, 2, api.PodRunning, extraLabelMap, rs, "podWithExtraLabel") @@ -316,7 +320,7 @@ func TestControllerUpdateReplicas(t *testing.T) { // 2. Status.FullyLabeledReplicas should equal to the number of pods that // has the extra labels, i.e., 2. // 3. Every update to the status should include the Generation of the spec. - rs.Status = extensions.ReplicaSetStatus{Replicas: 4, FullyLabeledReplicas: 2, ReadyReplicas: 4, ObservedGeneration: 1} + rs.Status = extensions.ReplicaSetStatus{Replicas: 4, FullyLabeledReplicas: 2, ReadyReplicas: 4, AvailableReplicas: 4, ObservedGeneration: 1} decRc := runtime.EncodeOrDie(testapi.Extensions.Codec(), rs) fakeHandler.ValidateRequest(t, testapi.Extensions.ResourcePath(replicaSetResourceName(), rs.Namespace, rs.Name)+"/status", "PUT", &decRc) @@ -346,12 +350,14 @@ func TestSyncReplicaSetDormancy(t *testing.T) { // Creates a replica and sets expectations rsSpec.Status.Replicas = 1 rsSpec.Status.ReadyReplicas = 1 + rsSpec.Status.AvailableReplicas = 1 manager.syncReplicaSet(getKey(rsSpec, t)) validateSyncReplicaSet(t, &fakePodControl, 1, 0, 0) // Expectations prevents replicas but not an update on status rsSpec.Status.Replicas = 0 rsSpec.Status.ReadyReplicas = 0 + rsSpec.Status.AvailableReplicas = 0 fakePodControl.Clear() manager.syncReplicaSet(getKey(rsSpec, t)) validateSyncReplicaSet(t, &fakePodControl, 0, 0, 0) @@ -367,6 +373,7 @@ func TestSyncReplicaSetDormancy(t *testing.T) { manager.expectations.CreationObserved(rsKey) rsSpec.Status.Replicas = 1 rsSpec.Status.ReadyReplicas = 1 + rsSpec.Status.AvailableReplicas = 1 fakePodControl.Clear() fakePodControl.Err = fmt.Errorf("Fake Error") @@ -662,7 +669,7 @@ func TestControllerUpdateStatusWithFailure(t *testing.T) { }) fakeRSClient := fakeClient.Extensions().ReplicaSets("default") numReplicas := 10 - updateReplicaCount(fakeRSClient, *rs, numReplicas, 0, 0) + updateReplicaCount(fakeRSClient, *rs, numReplicas, 0, 0, 0) updates, gets := 0, 0 for _, a := range fakeClient.Actions() { if a.GetResource().Resource != "replicasets" { @@ -1098,7 +1105,7 @@ func TestDoNotPatchPodWithOtherControlRef(t *testing.T) { var trueVar = true otherControllerReference := api.OwnerReference{UID: uuid.NewUUID(), APIVersion: "v1beta1", Kind: "ReplicaSet", Name: "AnotherRS", Controller: &trueVar} // add to podStore a matching Pod controlled by another controller. Expect no patch. - pod := newPod("pod", rs, api.PodRunning) + pod := newPod("pod", rs, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{otherControllerReference} manager.podStore.Indexer.Add(pod) err := manager.syncReplicaSet(getKey(rs, t)) @@ -1118,7 +1125,7 @@ func TestPatchPodWithOtherOwnerRef(t *testing.T) { // ref, but has an owner ref pointing to other object. Expect a patch to // take control of it. unrelatedOwnerReference := api.OwnerReference{UID: uuid.NewUUID(), APIVersion: "batch/v1", Kind: "Job", Name: "Job"} - pod := newPod("pod", rs, api.PodRunning) + pod := newPod("pod", rs, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{unrelatedOwnerReference} manager.podStore.Indexer.Add(pod) @@ -1138,7 +1145,7 @@ func TestPatchPodWithCorrectOwnerRef(t *testing.T) { // add to podStore a matching pod that has an ownerRef pointing to the rs, // but ownerRef.Controller is false. Expect a patch to take control it. rsOwnerReference := api.OwnerReference{UID: rs.UID, APIVersion: "v1", Kind: "ReplicaSet", Name: rs.Name} - pod := newPod("pod", rs, api.PodRunning) + pod := newPod("pod", rs, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{rsOwnerReference} manager.podStore.Indexer.Add(pod) @@ -1157,8 +1164,8 @@ func TestPatchPodFails(t *testing.T) { manager.rsStore.Store.Add(rs) // add to podStore two matching pods. Expect two patches to take control // them. - manager.podStore.Indexer.Add(newPod("pod1", rs, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod2", rs, api.PodRunning)) + manager.podStore.Indexer.Add(newPod("pod1", rs, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod2", rs, api.PodRunning, nil)) // let both patches fail. The rs controller will assume it fails to take // control of the pods and create new ones. fakePodControl.Err = fmt.Errorf("Fake Error") @@ -1177,9 +1184,9 @@ func TestPatchExtraPodsThenDelete(t *testing.T) { manager.rsStore.Store.Add(rs) // add to podStore three matching pods. Expect three patches to take control // them, and later delete one of them. - manager.podStore.Indexer.Add(newPod("pod1", rs, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod2", rs, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod3", rs, api.PodRunning)) + manager.podStore.Indexer.Add(newPod("pod1", rs, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod2", rs, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod3", rs, api.PodRunning, nil)) err := manager.syncReplicaSet(getKey(rs, t)) if err != nil { t.Fatal(err) @@ -1194,7 +1201,7 @@ func TestUpdateLabelsRemoveControllerRef(t *testing.T) { rs := newReplicaSet(2, labelMap) manager.rsStore.Store.Add(rs) // put one pod in the podStore - pod := newPod("pod", rs, api.PodRunning) + pod := newPod("pod", rs, api.PodRunning, nil) pod.ResourceVersion = "1" var trueVar = true rsOwnerReference := api.OwnerReference{UID: rs.UID, APIVersion: "v1beta1", Kind: "ReplicaSet", Name: rs.Name, Controller: &trueVar} @@ -1268,7 +1275,7 @@ func TestDoNotAdoptOrCreateIfBeingDeleted(t *testing.T) { now := unversioned.Now() rs.DeletionTimestamp = &now manager.rsStore.Store.Add(rs) - pod1 := newPod("pod1", rs, api.PodRunning) + pod1 := newPod("pod1", rs, api.PodRunning, nil) manager.podStore.Indexer.Add(pod1) // no patch, no create @@ -1295,7 +1302,7 @@ func TestReadyReplicas(t *testing.T) { // Status.Replica should update to match number of pods in system, 1 new pod should be created. labelMap := map[string]string{"foo": "bar"} rs := newReplicaSet(2, labelMap) - rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 0, ObservedGeneration: 1} + rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 0, AvailableReplicas: 0, ObservedGeneration: 1} rs.Generation = 1 manager.rsStore.Store.Add(rs) @@ -1312,7 +1319,56 @@ func TestReadyReplicas(t *testing.T) { manager.syncReplicaSet(getKey(rs, t)) // ReadyReplicas should go from 0 to 2. - rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 2, ObservedGeneration: 1} + rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, ObservedGeneration: 1} + + decRs := runtime.EncodeOrDie(testapi.Extensions.Codec(), rs) + fakeHandler.ValidateRequest(t, testapi.Extensions.ResourcePath(replicaSetResourceName(), rs.Namespace, rs.Name)+"/status", "PUT", &decRs) + validateSyncReplicaSet(t, &fakePodControl, 0, 0, 0) +} + +func TestAvailableReplicas(t *testing.T) { + // This is a happy server just to record the PUT request we expect for status.Replicas + fakeHandler := utiltesting.FakeHandler{ + StatusCode: 200, + ResponseBody: "{}", + } + testServer := httptest.NewServer(&fakeHandler) + defer testServer.Close() + + client := clientset.NewForConfigOrDie(&restclient.Config{Host: testServer.URL, ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}) + manager := NewReplicaSetControllerFromClient(client, controller.NoResyncPeriodFunc, BurstReplicas, 0) + manager.podStoreSynced = alwaysReady + + // Status.Replica should update to match number of pods in system, 1 new pod should be created. + labelMap := map[string]string{"foo": "bar"} + rs := newReplicaSet(2, labelMap) + rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 0, AvailableReplicas: 0, ObservedGeneration: 1} + rs.Generation = 1 + // minReadySeconds set to 15s + rs.Spec.MinReadySeconds = 15 + manager.rsStore.Store.Add(rs) + + // First pod becomes ready 20s ago + moment := unversioned.Time{Time: time.Now().Add(-2e10)} + pod := newPod("pod", rs, api.PodRunning, &moment) + manager.podStore.Indexer.Add(pod) + + // Second pod becomes ready now + otherMoment := unversioned.Now() + otherPod := newPod("otherPod", rs, api.PodRunning, &otherMoment) + manager.podStore.Indexer.Add(otherPod) + + // This response body is just so we don't err out decoding the http response + response := runtime.EncodeOrDie(testapi.Extensions.Codec(), &extensions.ReplicaSet{}) + fakeHandler.ResponseBody = response + + fakePodControl := controller.FakePodControl{} + manager.podControl = &fakePodControl + + // The controller should see only one available pod. + manager.syncReplicaSet(getKey(rs, t)) + + rs.Status = extensions.ReplicaSetStatus{Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 1, ObservedGeneration: 1} decRs := runtime.EncodeOrDie(testapi.Extensions.Codec(), rs) fakeHandler.ValidateRequest(t, testapi.Extensions.ResourcePath(replicaSetResourceName(), rs.Namespace, rs.Name)+"/status", "PUT", &decRs) diff --git a/pkg/controller/replicaset/replica_set_utils.go b/pkg/controller/replicaset/replica_set_utils.go index 60037877375..edaf8a8b326 100644 --- a/pkg/controller/replicaset/replica_set_utils.go +++ b/pkg/controller/replicaset/replica_set_utils.go @@ -27,13 +27,14 @@ import ( ) // updateReplicaCount attempts to update the Status.Replicas of the given ReplicaSet, with a single GET/PUT retry. -func updateReplicaCount(rsClient client.ReplicaSetInterface, rs extensions.ReplicaSet, numReplicas, numFullyLabeledReplicas, numReadyReplicas int) (updateErr error) { +func updateReplicaCount(rsClient client.ReplicaSetInterface, rs extensions.ReplicaSet, numReplicas, numFullyLabeledReplicas, numReadyReplicas, numAvailableReplicas int) (updateErr error) { // This is the steady state. It happens when the ReplicaSet doesn't have any expectations, since // we do a periodic relist every 30s. If the generations differ but the replicas are // the same, a caller might've resized to the same replica count. if int(rs.Status.Replicas) == numReplicas && int(rs.Status.FullyLabeledReplicas) == numFullyLabeledReplicas && int(rs.Status.ReadyReplicas) == numReadyReplicas && + int(rs.Status.AvailableReplicas) == numAvailableReplicas && rs.Generation == rs.Status.ObservedGeneration { return nil } @@ -49,12 +50,14 @@ func updateReplicaCount(rsClient client.ReplicaSetInterface, rs extensions.Repli fmt.Sprintf("replicas %d->%d (need %d), ", rs.Status.Replicas, numReplicas, rs.Spec.Replicas) + fmt.Sprintf("fullyLabeledReplicas %d->%d, ", rs.Status.FullyLabeledReplicas, numFullyLabeledReplicas) + fmt.Sprintf("readyReplicas %d->%d, ", rs.Status.ReadyReplicas, numReadyReplicas) + + fmt.Sprintf("availableReplicas %d->%d, ", rs.Status.AvailableReplicas, numAvailableReplicas) + fmt.Sprintf("sequence No: %v->%v", rs.Status.ObservedGeneration, generation)) rs.Status = extensions.ReplicaSetStatus{ Replicas: int32(numReplicas), FullyLabeledReplicas: int32(numFullyLabeledReplicas), ReadyReplicas: int32(numReadyReplicas), + AvailableReplicas: int32(numAvailableReplicas), ObservedGeneration: generation, } _, updateErr = rsClient.UpdateStatus(rs) diff --git a/pkg/controller/replication/replication_controller.go b/pkg/controller/replication/replication_controller.go index 121ccb4e784..c66525cfd8a 100644 --- a/pkg/controller/replication/replication_controller.go +++ b/pkg/controller/replication/replication_controller.go @@ -717,6 +717,7 @@ func (rm *ReplicationManager) syncReplicationController(key string) error { // matching pods must be part of the filteredPods. fullyLabeledReplicasCount := 0 readyReplicasCount := 0 + availableReplicasCount := 0 templateLabel := labels.Set(rc.Spec.Template.Labels).AsSelectorPreValidated() for _, pod := range filteredPods { if templateLabel.Matches(labels.Set(pod.Labels)) { @@ -724,11 +725,21 @@ func (rm *ReplicationManager) syncReplicationController(key string) error { } if api.IsPodReady(pod) { readyReplicasCount++ + if api.IsPodAvailable(pod, rc.Spec.MinReadySeconds, unversioned.Now()) { + availableReplicasCount++ + } } } // Always updates status as pods come up or die. - if err := updateReplicaCount(rm.kubeClient.Core().ReplicationControllers(rc.Namespace), rc, len(filteredPods), fullyLabeledReplicasCount, readyReplicasCount); err != nil { + if err := updateReplicaCount( + rm.kubeClient.Core().ReplicationControllers(rc.Namespace), + rc, + len(filteredPods), + fullyLabeledReplicasCount, + readyReplicasCount, + availableReplicasCount, + ); err != nil { // Multiple things could lead to this update failing. Returning an error causes a requeue without forcing a hotloop return err } diff --git a/pkg/controller/replication/replication_controller_test.go b/pkg/controller/replication/replication_controller_test.go index d1cf72baac6..fc0d16d13db 100644 --- a/pkg/controller/replication/replication_controller_test.go +++ b/pkg/controller/replication/replication_controller_test.go @@ -97,10 +97,14 @@ func newReplicationController(replicas int) *api.ReplicationController { } // create a pod with the given phase for the given rc (same selectors and namespace). -func newPod(name string, rc *api.ReplicationController, status api.PodPhase) *api.Pod { +func newPod(name string, rc *api.ReplicationController, status api.PodPhase, lastTransitionTime *unversioned.Time) *api.Pod { var conditions []api.PodCondition if status == api.PodRunning { - conditions = append(conditions, api.PodCondition{Type: api.PodReady, Status: api.ConditionTrue}) + condition := api.PodCondition{Type: api.PodReady, Status: api.ConditionTrue} + if lastTransitionTime != nil { + condition.LastTransitionTime = *lastTransitionTime + } + conditions = append(conditions, condition) } return &api.Pod{ ObjectMeta: api.ObjectMeta{ @@ -118,7 +122,7 @@ func newPodList(store cache.Store, count int, status api.PodPhase, rc *api.Repli var trueVar = true controllerReference := api.OwnerReference{UID: rc.UID, APIVersion: "v1", Kind: "ReplicationController", Name: rc.Name, Controller: &trueVar} for i := 0; i < count; i++ { - pod := newPod(fmt.Sprintf("%s%d", name, i), rc, status) + pod := newPod(fmt.Sprintf("%s%d", name, i), rc, status, nil) pod.OwnerReferences = []api.OwnerReference{controllerReference} if store != nil { store.Add(pod) @@ -247,7 +251,7 @@ func TestStatusUpdatesWithoutReplicasChange(t *testing.T) { activePods := 5 rc := newReplicationController(activePods) manager.rcStore.Indexer.Add(rc) - rc.Status = api.ReplicationControllerStatus{Replicas: int32(activePods), ReadyReplicas: int32(activePods)} + rc.Status = api.ReplicationControllerStatus{Replicas: int32(activePods), ReadyReplicas: int32(activePods), AvailableReplicas: int32(activePods)} newPodList(manager.podStore.Indexer, activePods, api.PodRunning, rc, "pod") fakePodControl := controller.FakePodControl{} @@ -288,7 +292,7 @@ func TestControllerUpdateReplicas(t *testing.T) { // Status.Replica should update to match number of pods in system, 1 new pod should be created. rc := newReplicationController(5) manager.rcStore.Indexer.Add(rc) - rc.Status = api.ReplicationControllerStatus{Replicas: 2, FullyLabeledReplicas: 6, ReadyReplicas: 2, ObservedGeneration: 0} + rc.Status = api.ReplicationControllerStatus{Replicas: 2, FullyLabeledReplicas: 6, ReadyReplicas: 2, AvailableReplicas: 2, ObservedGeneration: 0} rc.Generation = 1 newPodList(manager.podStore.Indexer, 2, api.PodRunning, rc, "pod") rcCopy := *rc @@ -309,7 +313,7 @@ func TestControllerUpdateReplicas(t *testing.T) { // 2. Status.FullyLabeledReplicas should equal to the number of pods that // has the extra labels, i.e., 2. // 3. Every update to the status should include the Generation of the spec. - rc.Status = api.ReplicationControllerStatus{Replicas: 4, ReadyReplicas: 4, ObservedGeneration: 1} + rc.Status = api.ReplicationControllerStatus{Replicas: 4, ReadyReplicas: 4, AvailableReplicas: 4, ObservedGeneration: 1} decRc := runtime.EncodeOrDie(testapi.Default.Codec(), rc) fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name)+"/status", "PUT", &decRc) @@ -337,12 +341,14 @@ func TestSyncReplicationControllerDormancy(t *testing.T) { // Creates a replica and sets expectations controllerSpec.Status.Replicas = 1 controllerSpec.Status.ReadyReplicas = 1 + controllerSpec.Status.AvailableReplicas = 1 manager.syncReplicationController(getKey(controllerSpec, t)) validateSyncReplication(t, &fakePodControl, 1, 0, 0) // Expectations prevents replicas but not an update on status controllerSpec.Status.Replicas = 0 controllerSpec.Status.ReadyReplicas = 0 + controllerSpec.Status.AvailableReplicas = 0 fakePodControl.Clear() manager.syncReplicationController(getKey(controllerSpec, t)) validateSyncReplication(t, &fakePodControl, 0, 0, 0) @@ -358,6 +364,7 @@ func TestSyncReplicationControllerDormancy(t *testing.T) { manager.expectations.CreationObserved(rcKey) controllerSpec.Status.Replicas = 1 controllerSpec.Status.ReadyReplicas = 1 + controllerSpec.Status.AvailableReplicas = 1 fakePodControl.Clear() fakePodControl.Err = fmt.Errorf("Fake Error") @@ -634,7 +641,7 @@ func TestControllerUpdateStatusWithFailure(t *testing.T) { }) fakeRCClient := c.Core().ReplicationControllers("default") numReplicas := 10 - updateReplicaCount(fakeRCClient, *rc, numReplicas, 0, 0) + updateReplicaCount(fakeRCClient, *rc, numReplicas, 0, 0, 0) updates, gets := 0, 0 for _, a := range c.Actions() { if a.GetResource().Resource != "replicationcontrollers" { @@ -1149,7 +1156,7 @@ func TestDoNotPatchPodWithOtherControlRef(t *testing.T) { var trueVar = true otherControllerReference := api.OwnerReference{UID: uuid.NewUUID(), APIVersion: "v1", Kind: "ReplicationController", Name: "AnotherRC", Controller: &trueVar} // add to podStore a matching Pod controlled by another controller. Expect no patch. - pod := newPod("pod", rc, api.PodRunning) + pod := newPod("pod", rc, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{otherControllerReference} manager.podStore.Indexer.Add(pod) err := manager.syncReplicationController(getKey(rc, t)) @@ -1168,7 +1175,7 @@ func TestPatchPodWithOtherOwnerRef(t *testing.T) { // ref, but has an owner ref pointing to other object. Expect a patch to // take control of it. unrelatedOwnerReference := api.OwnerReference{UID: uuid.NewUUID(), APIVersion: "batch/v1", Kind: "Job", Name: "Job"} - pod := newPod("pod", rc, api.PodRunning) + pod := newPod("pod", rc, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{unrelatedOwnerReference} manager.podStore.Indexer.Add(pod) @@ -1187,7 +1194,7 @@ func TestPatchPodWithCorrectOwnerRef(t *testing.T) { // add to podStore a matching pod that has an ownerRef pointing to the rc, // but ownerRef.Controller is false. Expect a patch to take control it. rcOwnerReference := api.OwnerReference{UID: rc.UID, APIVersion: "v1", Kind: "ReplicationController", Name: rc.Name} - pod := newPod("pod", rc, api.PodRunning) + pod := newPod("pod", rc, api.PodRunning, nil) pod.OwnerReferences = []api.OwnerReference{rcOwnerReference} manager.podStore.Indexer.Add(pod) @@ -1205,8 +1212,8 @@ func TestPatchPodFails(t *testing.T) { manager.rcStore.Indexer.Add(rc) // add to podStore two matching pods. Expect two patches to take control // them. - manager.podStore.Indexer.Add(newPod("pod1", rc, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod2", rc, api.PodRunning)) + manager.podStore.Indexer.Add(newPod("pod1", rc, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod2", rc, api.PodRunning, nil)) // let both patches fail. The rc manager will assume it fails to take // control of the pods and create new ones. fakePodControl.Err = fmt.Errorf("Fake Error") @@ -1224,9 +1231,9 @@ func TestPatchExtraPodsThenDelete(t *testing.T) { manager.rcStore.Indexer.Add(rc) // add to podStore three matching pods. Expect three patches to take control // them, and later delete one of them. - manager.podStore.Indexer.Add(newPod("pod1", rc, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod2", rc, api.PodRunning)) - manager.podStore.Indexer.Add(newPod("pod3", rc, api.PodRunning)) + manager.podStore.Indexer.Add(newPod("pod1", rc, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod2", rc, api.PodRunning, nil)) + manager.podStore.Indexer.Add(newPod("pod3", rc, api.PodRunning, nil)) err := manager.syncReplicationController(getKey(rc, t)) if err != nil { t.Fatal(err) @@ -1240,7 +1247,7 @@ func TestUpdateLabelsRemoveControllerRef(t *testing.T) { rc := newReplicationController(2) manager.rcStore.Indexer.Add(rc) // put one pod in the podStore - pod := newPod("pod", rc, api.PodRunning) + pod := newPod("pod", rc, api.PodRunning, nil) pod.ResourceVersion = "1" var trueVar = true rcOwnerReference := api.OwnerReference{UID: rc.UID, APIVersion: "v1", Kind: "ReplicationController", Name: rc.Name, Controller: &trueVar} @@ -1312,7 +1319,7 @@ func TestDoNotAdoptOrCreateIfBeingDeleted(t *testing.T) { now := unversioned.Now() rc.DeletionTimestamp = &now manager.rcStore.Indexer.Add(rc) - pod1 := newPod("pod1", rc, api.PodRunning) + pod1 := newPod("pod1", rc, api.PodRunning, nil) manager.podStore.Indexer.Add(pod1) // no patch, no create @@ -1338,7 +1345,7 @@ func TestReadyReplicas(t *testing.T) { // Status.Replica should update to match number of pods in system, 1 new pod should be created. rc := newReplicationController(2) - rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 0, ObservedGeneration: 1} + rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 0, AvailableReplicas: 0, ObservedGeneration: 1} rc.Generation = 1 manager.rcStore.Indexer.Add(rc) @@ -1355,7 +1362,55 @@ func TestReadyReplicas(t *testing.T) { manager.syncReplicationController(getKey(rc, t)) // ReadyReplicas should go from 0 to 2. - rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 2, ObservedGeneration: 1} + rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, ObservedGeneration: 1} + + decRc := runtime.EncodeOrDie(testapi.Default.Codec(), rc) + fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name)+"/status", "PUT", &decRc) + validateSyncReplication(t, &fakePodControl, 0, 0, 0) +} + +func TestAvailableReplicas(t *testing.T) { + // This is a happy server just to record the PUT request we expect for status.Replicas + fakeHandler := utiltesting.FakeHandler{ + StatusCode: 200, + ResponseBody: "{}", + } + testServer := httptest.NewServer(&fakeHandler) + defer testServer.Close() + + c := clientset.NewForConfigOrDie(&restclient.Config{Host: testServer.URL, ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}}) + manager := NewReplicationManagerFromClient(c, controller.NoResyncPeriodFunc, BurstReplicas, 0) + manager.podStoreSynced = alwaysReady + + // Status.Replica should update to match number of pods in system, 1 new pod should be created. + rc := newReplicationController(2) + rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 0, ObservedGeneration: 1} + rc.Generation = 1 + // minReadySeconds set to 15s + rc.Spec.MinReadySeconds = 15 + manager.rcStore.Indexer.Add(rc) + + // First pod becomes ready 20s ago + moment := unversioned.Time{Time: time.Now().Add(-2e10)} + pod := newPod("pod", rc, api.PodRunning, &moment) + manager.podStore.Indexer.Add(pod) + + // Second pod becomes ready now + otherMoment := unversioned.Now() + otherPod := newPod("otherPod", rc, api.PodRunning, &otherMoment) + manager.podStore.Indexer.Add(otherPod) + + // This response body is just so we don't err out decoding the http response + response := runtime.EncodeOrDie(testapi.Default.Codec(), &api.ReplicationController{}) + fakeHandler.ResponseBody = response + + fakePodControl := controller.FakePodControl{} + manager.podControl = &fakePodControl + + // The controller should see only one available pod. + manager.syncReplicationController(getKey(rc, t)) + + rc.Status = api.ReplicationControllerStatus{Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 1, ObservedGeneration: 1} decRc := runtime.EncodeOrDie(testapi.Default.Codec(), rc) fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name)+"/status", "PUT", &decRc) diff --git a/pkg/controller/replication/replication_controller_utils.go b/pkg/controller/replication/replication_controller_utils.go index a5ea91304da..d3924ee6c5a 100644 --- a/pkg/controller/replication/replication_controller_utils.go +++ b/pkg/controller/replication/replication_controller_utils.go @@ -27,13 +27,14 @@ import ( ) // updateReplicaCount attempts to update the Status.Replicas of the given controller, with a single GET/PUT retry. -func updateReplicaCount(rcClient unversionedcore.ReplicationControllerInterface, controller api.ReplicationController, numReplicas, numFullyLabeledReplicas, numReadyReplicas int) (updateErr error) { +func updateReplicaCount(rcClient unversionedcore.ReplicationControllerInterface, controller api.ReplicationController, numReplicas, numFullyLabeledReplicas, numReadyReplicas, numAvailableReplicas int) (updateErr error) { // This is the steady state. It happens when the rc doesn't have any expectations, since // we do a periodic relist every 30s. If the generations differ but the replicas are // the same, a caller might've resized to the same replica count. if int(controller.Status.Replicas) == numReplicas && int(controller.Status.FullyLabeledReplicas) == numFullyLabeledReplicas && int(controller.Status.ReadyReplicas) == numReadyReplicas && + int(controller.Status.AvailableReplicas) == numAvailableReplicas && controller.Generation == controller.Status.ObservedGeneration { return nil } @@ -49,12 +50,14 @@ func updateReplicaCount(rcClient unversionedcore.ReplicationControllerInterface, fmt.Sprintf("replicas %d->%d (need %d), ", controller.Status.Replicas, numReplicas, controller.Spec.Replicas) + fmt.Sprintf("fullyLabeledReplicas %d->%d, ", controller.Status.FullyLabeledReplicas, numFullyLabeledReplicas) + fmt.Sprintf("readyReplicas %d->%d, ", controller.Status.ReadyReplicas, numReadyReplicas) + + fmt.Sprintf("availableReplicas %d->%d, ", controller.Status.AvailableReplicas, numAvailableReplicas) + fmt.Sprintf("sequence No: %v->%v", controller.Status.ObservedGeneration, generation)) rc.Status = api.ReplicationControllerStatus{ Replicas: int32(numReplicas), FullyLabeledReplicas: int32(numFullyLabeledReplicas), ReadyReplicas: int32(numReadyReplicas), + AvailableReplicas: int32(numAvailableReplicas), ObservedGeneration: generation, } _, updateErr = rcClient.UpdateStatus(rc)