diff --git a/pkg/apis/apps/v1beta1/types.go b/pkg/apis/apps/v1beta1/types.go index e3cb013acda..a4b1cdc6695 100644 --- a/pkg/apis/apps/v1beta1/types.go +++ b/pkg/apis/apps/v1beta1/types.go @@ -356,6 +356,12 @@ type DeploymentStatus struct { // +patchMergeKey=type // +patchStrategy=merge Conditions []DeploymentCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` + + // Count of hash collisions for the Deployment. The Deployment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int64 `json:"collisionCount,omitempty"` } type DeploymentConditionType string diff --git a/pkg/apis/extensions/types.go b/pkg/apis/extensions/types.go index 71731a41623..7c2c1c2856e 100644 --- a/pkg/apis/extensions/types.go +++ b/pkg/apis/extensions/types.go @@ -327,6 +327,12 @@ type DeploymentStatus struct { // Represents the latest available observations of a deployment's current state. Conditions []DeploymentCondition + + // Count of hash collisions for the Deployment. The Deployment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int64 } type DeploymentConditionType string diff --git a/pkg/apis/extensions/v1beta1/types.go b/pkg/apis/extensions/v1beta1/types.go index 17838877c3c..f9eaf75437f 100644 --- a/pkg/apis/extensions/v1beta1/types.go +++ b/pkg/apis/extensions/v1beta1/types.go @@ -325,6 +325,12 @@ type DeploymentStatus struct { // +patchMergeKey=type // +patchStrategy=merge Conditions []DeploymentCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` + + // Count of hash collisions for the Deployment. The Deployment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int64 `json:"collisionCount,omitempty"` } type DeploymentConditionType string diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index fa5cefca5cd..514faa2adbc 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -346,6 +346,9 @@ func ValidateDeploymentStatus(status *extensions.DeploymentStatus, fldPath *fiel allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ReadyReplicas), fldPath.Child("readyReplicas"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.AvailableReplicas), fldPath.Child("availableReplicas"))...) allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UnavailableReplicas), fldPath.Child("unavailableReplicas"))...) + if status.CollisionCount != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(*status.CollisionCount, fldPath.Child("collisionCount"))...) + } msg := "cannot be greater than status.replicas" if status.UpdatedReplicas > status.Replicas { allErrs = append(allErrs, field.Invalid(fldPath.Child("updatedReplicas"), status.UpdatedReplicas, msg)) @@ -372,10 +375,29 @@ func ValidateDeploymentUpdate(update, old *extensions.Deployment) field.ErrorLis func ValidateDeploymentStatusUpdate(update, old *extensions.Deployment) field.ErrorList { allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDeploymentStatus(&update.Status, field.NewPath("status"))...) + fldPath := field.NewPath("status") + allErrs = append(allErrs, ValidateDeploymentStatus(&update.Status, fldPath)...) + if isDecremented(update.Status.CollisionCount, old.Status.CollisionCount) { + value := int64(0) + if update.Status.CollisionCount != nil { + value = *update.Status.CollisionCount + } + allErrs = append(allErrs, field.Invalid(fldPath.Child("collisionCount"), value, "cannot be decremented")) + } return allErrs } +// TODO: Move in "k8s.io/kubernetes/pkg/api/validation" +func isDecremented(update, old *int64) bool { + if update == nil && old != nil { + return true + } + if update == nil || old == nil { + return false + } + return *update < *old +} + func ValidateDeployment(obj *extensions.Deployment) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName, field.NewPath("metadata")) allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, field.NewPath("spec"))...) diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index 8ded02a59ba..1ec5e8b0834 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -21,6 +21,8 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation/field" @@ -1230,6 +1232,11 @@ func TestValidateDeployment(t *testing.T) { } } +func int64p(i int) *int64 { + i64 := int64(i) + return &i64 +} + func TestValidateDeploymentStatus(t *testing.T) { tests := []struct { name string @@ -1239,6 +1246,7 @@ func TestValidateDeploymentStatus(t *testing.T) { readyReplicas int32 availableReplicas int32 observedGeneration int64 + collisionCount *int64 expectedErr bool }{ @@ -1335,6 +1343,13 @@ func TestValidateDeploymentStatus(t *testing.T) { observedGeneration: 1, expectedErr: false, }, + { + name: "invalid collisionCount", + replicas: 3, + observedGeneration: 1, + collisionCount: int64p(-3), + expectedErr: true, + }, } for _, test := range tests { @@ -1344,10 +1359,82 @@ func TestValidateDeploymentStatus(t *testing.T) { ReadyReplicas: test.readyReplicas, AvailableReplicas: test.availableReplicas, ObservedGeneration: test.observedGeneration, + CollisionCount: test.collisionCount, } - if hasErr := len(ValidateDeploymentStatus(&status, field.NewPath("status"))) > 0; hasErr != test.expectedErr { - t.Errorf("%s: expected error: %t, got error: %t", test.name, test.expectedErr, hasErr) + errs := ValidateDeploymentStatus(&status, field.NewPath("status")) + if hasErr := len(errs) > 0; hasErr != test.expectedErr { + errString := spew.Sprintf("%#v", errs) + t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errString) + } + } +} + +func TestValidateDeploymentStatusUpdate(t *testing.T) { + tests := []struct { + name string + + from, to extensions.DeploymentStatus + + expectedErr bool + }{ + { + name: "increase: valid update", + from: extensions.DeploymentStatus{ + CollisionCount: nil, + }, + to: extensions.DeploymentStatus{ + CollisionCount: int64p(1), + }, + expectedErr: false, + }, + { + name: "stable: valid update", + from: extensions.DeploymentStatus{ + CollisionCount: int64p(1), + }, + to: extensions.DeploymentStatus{ + CollisionCount: int64p(1), + }, + expectedErr: false, + }, + { + name: "unset: invalid update", + from: extensions.DeploymentStatus{ + CollisionCount: int64p(1), + }, + to: extensions.DeploymentStatus{ + CollisionCount: nil, + }, + expectedErr: true, + }, + { + name: "decrease: invalid update", + from: extensions.DeploymentStatus{ + CollisionCount: int64p(2), + }, + to: extensions.DeploymentStatus{ + CollisionCount: int64p(1), + }, + expectedErr: true, + }, + } + + for _, test := range tests { + meta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault, ResourceVersion: "1"} + from := &extensions.Deployment{ + ObjectMeta: meta, + Status: test.from, + } + to := &extensions.Deployment{ + ObjectMeta: meta, + Status: test.to, + } + + errs := ValidateDeploymentStatusUpdate(to, from) + if hasErr := len(errs) > 0; hasErr != test.expectedErr { + errString := spew.Sprintf("%#v", errs) + t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errString) } } }