mirror of https://github.com/k3s-io/k3s
ignore selector changes for deployment, replicaset and daemonset prior update
parent
4457e43e7b
commit
9929e03f87
|
@ -16,8 +16,12 @@ go_library(
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//pkg/apis/extensions/validation:go_default_library",
|
"//pkg/apis/extensions/validation:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
|
@ -31,6 +35,10 @@ go_test(
|
||||||
library = ":go_default_library",
|
library = ":go_default_library",
|
||||||
deps = [
|
deps = [
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
|
"//pkg/apis/extensions:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,8 +17,14 @@ limitations under the License.
|
||||||
package daemonset
|
package daemonset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||||
|
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
@ -109,9 +115,29 @@ func (daemonSetStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
|
||||||
// ValidateUpdate is the default update validation for an end user.
|
// ValidateUpdate is the default update validation for an end user.
|
||||||
func (daemonSetStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
func (daemonSetStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
validationErrorList := validation.ValidateDaemonSet(obj.(*extensions.DaemonSet))
|
newDaemonSet := obj.(*extensions.DaemonSet)
|
||||||
updateErrorList := validation.ValidateDaemonSetUpdate(obj.(*extensions.DaemonSet), old.(*extensions.DaemonSet))
|
oldDaemonSet := old.(*extensions.DaemonSet)
|
||||||
return append(validationErrorList, updateErrorList...)
|
allErrs := validation.ValidateDaemonSet(obj.(*extensions.DaemonSet))
|
||||||
|
allErrs = append(allErrs, validation.ValidateDaemonSetUpdate(newDaemonSet, oldDaemonSet)...)
|
||||||
|
|
||||||
|
// Update is not allowed to set Spec.Selector for all groups/versions except extensions/v1beta1.
|
||||||
|
// If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector)
|
||||||
|
// to prevent unintentionally breaking users who may rely on the old behavior.
|
||||||
|
// TODO(#50791): after extensions/v1beta1 is removed, move selector immutability check inside ValidateDaemonSetUpdate().
|
||||||
|
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
|
||||||
|
groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||||
|
switch groupVersion {
|
||||||
|
case extensionsv1beta1.SchemeGroupVersion:
|
||||||
|
// no-op for compatibility
|
||||||
|
case appsv1beta2.SchemeGroupVersion:
|
||||||
|
// disallow mutation of selector
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(newDaemonSet.Spec.Selector, oldDaemonSet.Spec.Selector, field.NewPath("spec").Child("selector"))...)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected group/version: %v", groupVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllowUnconditionalUpdate is the default update policy for daemon set objects.
|
// AllowUnconditionalUpdate is the default update policy for daemon set objects.
|
||||||
|
|
|
@ -17,10 +17,22 @@ limitations under the License.
|
||||||
package daemonset
|
package daemonset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
_ "k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fakeImageName = "fake-name"
|
||||||
|
fakeImage = "fakeimage"
|
||||||
|
daemonsetName = "test-daemonset"
|
||||||
|
namespace = "test-namespace"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultGarbageCollectionPolicy(t *testing.T) {
|
func TestDefaultGarbageCollectionPolicy(t *testing.T) {
|
||||||
|
@ -31,3 +43,84 @@ func TestDefaultGarbageCollectionPolicy(t *testing.T) {
|
||||||
t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
|
t.Errorf("DefaultGarbageCollectionPolicy() = %#v, want %#v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectorImmutability(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
requestInfo genericapirequest.RequestInfo
|
||||||
|
oldSelectorLabels map[string]string
|
||||||
|
newSelectorLabels map[string]string
|
||||||
|
expectedErrorList field.ErrorList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "apps",
|
||||||
|
APIVersion: "v1beta2",
|
||||||
|
Resource: "daemonsets",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{
|
||||||
|
&field.Error{
|
||||||
|
Type: field.ErrorTypeInvalid,
|
||||||
|
Field: field.NewPath("spec").Child("selector").String(),
|
||||||
|
BadValue: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"c": "d"},
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
Detail: "field is immutable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "extensions",
|
||||||
|
APIVersion: "v1beta1",
|
||||||
|
Resource: "daemonsets",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
oldDaemonSet := newDaemonSetWithSelectorLabels(test.oldSelectorLabels, 1)
|
||||||
|
newDaemonSet := newDaemonSetWithSelectorLabels(test.newSelectorLabels, 2)
|
||||||
|
context := genericapirequest.NewContext()
|
||||||
|
context = genericapirequest.WithRequestInfo(context, &test.requestInfo)
|
||||||
|
errorList := daemonSetStrategy{}.ValidateUpdate(context, newDaemonSet, oldDaemonSet)
|
||||||
|
if !reflect.DeepEqual(test.expectedErrorList, errorList) {
|
||||||
|
t.Errorf("Unexpected error list, expected: %v, actual: %v", test.expectedErrorList, errorList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDaemonSetWithSelectorLabels(selectorLabels map[string]string, templateGeneration int64) *extensions.DaemonSet {
|
||||||
|
return &extensions.DaemonSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: daemonsetName,
|
||||||
|
Namespace: namespace,
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Spec: extensions.DaemonSetSpec{
|
||||||
|
Selector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels,
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
UpdateStrategy: extensions.DaemonSetUpdateStrategy{
|
||||||
|
Type: extensions.OnDeleteDaemonSetStrategyType,
|
||||||
|
},
|
||||||
|
TemplateGeneration: templateGeneration,
|
||||||
|
Template: api.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: selectorLabels,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
RestartPolicy: api.RestartPolicyAlways,
|
||||||
|
DNSPolicy: api.DNSClusterFirst,
|
||||||
|
Containers: []api.Container{{Name: fakeImageName, Image: fakeImage, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,10 +17,15 @@ go_library(
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//pkg/apis/extensions/validation:go_default_library",
|
"//pkg/apis/extensions/validation:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/apps/v1beta1:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||||
|
@ -37,6 +42,8 @@ go_test(
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,8 +17,15 @@ limitations under the License.
|
||||||
package deployment
|
package deployment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||||
|
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||||
|
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
@ -88,7 +95,31 @@ func (deploymentStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, o
|
||||||
|
|
||||||
// ValidateUpdate is the default update validation for an end user.
|
// ValidateUpdate is the default update validation for an end user.
|
||||||
func (deploymentStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
func (deploymentStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
return validation.ValidateDeploymentUpdate(obj.(*extensions.Deployment), old.(*extensions.Deployment))
|
newDeployment := obj.(*extensions.Deployment)
|
||||||
|
oldDeployment := old.(*extensions.Deployment)
|
||||||
|
allErrs := validation.ValidateDeploymentUpdate(newDeployment, oldDeployment)
|
||||||
|
|
||||||
|
// Update is not allowed to set Spec.Selector for all groups/versions except extensions/v1beta1.
|
||||||
|
// If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector)
|
||||||
|
// to prevent unintentionally breaking users who may rely on the old behavior.
|
||||||
|
// TODO(#50791): after apps/v1beta1 and extensions/v1beta1 are removed,
|
||||||
|
// move selector immutability check inside ValidateDeploymentUpdate().
|
||||||
|
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
|
||||||
|
groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||||
|
switch groupVersion {
|
||||||
|
case appsv1beta1.SchemeGroupVersion:
|
||||||
|
// no-op for compatibility
|
||||||
|
case extensionsv1beta1.SchemeGroupVersion:
|
||||||
|
// no-op for compatibility
|
||||||
|
case appsv1beta2.SchemeGroupVersion:
|
||||||
|
// disallow mutation of selector
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(newDeployment.Spec.Selector, oldDeployment.Spec.Selector, field.NewPath("spec").Child("selector"))...)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected group/version: %v", groupVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (deploymentStrategy) AllowUnconditionalUpdate() bool {
|
func (deploymentStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
|
|
@ -22,11 +22,20 @@ import (
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fakeImageName = "fake-name"
|
||||||
|
fakeImage = "fakeimage"
|
||||||
|
deploymentName = "test-deployment"
|
||||||
|
namespace = "test-namespace"
|
||||||
|
)
|
||||||
|
|
||||||
func TestStatusUpdates(t *testing.T) {
|
func TestStatusUpdates(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
old runtime.Object
|
old runtime.Object
|
||||||
|
@ -78,3 +87,99 @@ func newDeployment(labels, annotations map[string]string) *extensions.Deployment
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectorImmutability(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
requestInfo genericapirequest.RequestInfo
|
||||||
|
oldSelectorLabels map[string]string
|
||||||
|
newSelectorLabels map[string]string
|
||||||
|
expectedErrorList field.ErrorList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "apps",
|
||||||
|
APIVersion: "v1beta2",
|
||||||
|
Resource: "deployments",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{
|
||||||
|
&field.Error{
|
||||||
|
Type: field.ErrorTypeInvalid,
|
||||||
|
Field: field.NewPath("spec").Child("selector").String(),
|
||||||
|
BadValue: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"c": "d"},
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
Detail: "field is immutable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "apps",
|
||||||
|
APIVersion: "v1beta1",
|
||||||
|
Resource: "deployments",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "extensions",
|
||||||
|
APIVersion: "v1beta1",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
oldDeployment := newDeploymentWithSelectorLabels(test.oldSelectorLabels)
|
||||||
|
newDeployment := newDeploymentWithSelectorLabels(test.newSelectorLabels)
|
||||||
|
context := genericapirequest.NewContext()
|
||||||
|
context = genericapirequest.WithRequestInfo(context, &test.requestInfo)
|
||||||
|
errorList := deploymentStrategy{}.ValidateUpdate(context, newDeployment, oldDeployment)
|
||||||
|
if len(test.expectedErrorList) == 0 && len(errorList) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(test.expectedErrorList, errorList) {
|
||||||
|
t.Errorf("Unexpected error list, expected: %v, actual: %v", test.expectedErrorList, errorList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeploymentWithSelectorLabels(selectorLabels map[string]string) *extensions.Deployment {
|
||||||
|
return &extensions.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: deploymentName,
|
||||||
|
Namespace: namespace,
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Spec: extensions.DeploymentSpec{
|
||||||
|
Selector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels,
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
Strategy: extensions.DeploymentStrategy{
|
||||||
|
Type: extensions.RollingUpdateDeploymentStrategyType,
|
||||||
|
RollingUpdate: &extensions.RollingUpdateDeployment{
|
||||||
|
MaxSurge: intstr.FromInt(1),
|
||||||
|
MaxUnavailable: intstr.FromInt(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Template: api.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: selectorLabels,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
RestartPolicy: api.RestartPolicyAlways,
|
||||||
|
DNSPolicy: api.DNSDefault,
|
||||||
|
Containers: []api.Container{{Name: fakeImageName, Image: fakeImage, ImagePullPolicy: api.PullNever, TerminationMessagePolicy: api.TerminationMessageReadFile}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,12 +17,16 @@ go_library(
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//pkg/apis/extensions/validation:go_default_library",
|
"//pkg/apis/extensions/validation:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/apps/v1beta2:go_default_library",
|
||||||
|
"//vendor/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
|
@ -41,6 +45,7 @@ go_test(
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/apis/extensions:go_default_library",
|
"//pkg/apis/extensions:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||||
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,10 +22,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||||
|
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
|
@ -103,9 +107,29 @@ func (rsStrategy) AllowCreateOnUpdate() bool {
|
||||||
|
|
||||||
// ValidateUpdate is the default update validation for an end user.
|
// ValidateUpdate is the default update validation for an end user.
|
||||||
func (rsStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
func (rsStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||||
validationErrorList := validation.ValidateReplicaSet(obj.(*extensions.ReplicaSet))
|
newReplicaSet := obj.(*extensions.ReplicaSet)
|
||||||
updateErrorList := validation.ValidateReplicaSetUpdate(obj.(*extensions.ReplicaSet), old.(*extensions.ReplicaSet))
|
oldReplicaSet := old.(*extensions.ReplicaSet)
|
||||||
return append(validationErrorList, updateErrorList...)
|
allErrs := validation.ValidateReplicaSet(obj.(*extensions.ReplicaSet))
|
||||||
|
allErrs = append(allErrs, validation.ValidateReplicaSetUpdate(newReplicaSet, oldReplicaSet)...)
|
||||||
|
|
||||||
|
// Update is not allowed to set Spec.Selector for all groups/versions except extensions/v1beta1.
|
||||||
|
// If RequestInfo is nil, it is better to revert to old behavior (i.e. allow update to set Spec.Selector)
|
||||||
|
// to prevent unintentionally breaking users who may rely on the old behavior.
|
||||||
|
// TODO(#50791): after extensions/v1beta1 is removed, move selector immutability check inside ValidateReplicaSetUpdate().
|
||||||
|
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
|
||||||
|
groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
|
||||||
|
switch groupVersion {
|
||||||
|
case extensionsv1beta1.SchemeGroupVersion:
|
||||||
|
// no-op for compatibility
|
||||||
|
case appsv1beta2.SchemeGroupVersion:
|
||||||
|
// disallow mutation of selector
|
||||||
|
allErrs = append(allErrs, apivalidation.ValidateImmutableField(newReplicaSet.Spec.Selector, oldReplicaSet.Spec.Selector, field.NewPath("spec").Child("selector"))...)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected group/version: %v", groupVersion))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rsStrategy) AllowUnconditionalUpdate() bool {
|
func (rsStrategy) AllowUnconditionalUpdate() bool {
|
||||||
|
|
|
@ -17,14 +17,23 @@ limitations under the License.
|
||||||
package replicaset
|
package replicaset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fakeImageName = "fake-name"
|
||||||
|
fakeImage = "fakeimage"
|
||||||
|
replicasetName = "test-replicaset"
|
||||||
|
namespace = "test-namespace"
|
||||||
|
)
|
||||||
|
|
||||||
func TestReplicaSetStrategy(t *testing.T) {
|
func TestReplicaSetStrategy(t *testing.T) {
|
||||||
ctx := genericapirequest.NewDefaultContext()
|
ctx := genericapirequest.NewDefaultContext()
|
||||||
if !Strategy.NamespaceScoped() {
|
if !Strategy.NamespaceScoped() {
|
||||||
|
@ -141,3 +150,80 @@ func TestReplicaSetStatusStrategy(t *testing.T) {
|
||||||
t.Errorf("Unexpected error %v", errs)
|
t.Errorf("Unexpected error %v", errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectorImmutability(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
requestInfo genericapirequest.RequestInfo
|
||||||
|
oldSelectorLabels map[string]string
|
||||||
|
newSelectorLabels map[string]string
|
||||||
|
expectedErrorList field.ErrorList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "apps",
|
||||||
|
APIVersion: "v1beta2",
|
||||||
|
Resource: "replicasets",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{
|
||||||
|
&field.Error{
|
||||||
|
Type: field.ErrorTypeInvalid,
|
||||||
|
Field: field.NewPath("spec").Child("selector").String(),
|
||||||
|
BadValue: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{"c": "d"},
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
Detail: "field is immutable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
genericapirequest.RequestInfo{
|
||||||
|
APIGroup: "extensions",
|
||||||
|
APIVersion: "v1beta1",
|
||||||
|
Resource: "replicasets",
|
||||||
|
},
|
||||||
|
map[string]string{"a": "b"},
|
||||||
|
map[string]string{"c": "d"},
|
||||||
|
field.ErrorList{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
oldReplicaSet := newReplicaSetWithSelectorLabels(test.oldSelectorLabels)
|
||||||
|
newReplicaSet := newReplicaSetWithSelectorLabels(test.newSelectorLabels)
|
||||||
|
context := genericapirequest.NewContext()
|
||||||
|
context = genericapirequest.WithRequestInfo(context, &test.requestInfo)
|
||||||
|
errorList := rsStrategy{}.ValidateUpdate(context, newReplicaSet, oldReplicaSet)
|
||||||
|
if !reflect.DeepEqual(test.expectedErrorList, errorList) {
|
||||||
|
t.Errorf("Unexpected error list, expected: %v, actual: %v", test.expectedErrorList, errorList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReplicaSetWithSelectorLabels(selectorLabels map[string]string) *extensions.ReplicaSet {
|
||||||
|
return &extensions.ReplicaSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: replicasetName,
|
||||||
|
Namespace: namespace,
|
||||||
|
ResourceVersion: "1",
|
||||||
|
},
|
||||||
|
Spec: extensions.ReplicaSetSpec{
|
||||||
|
Selector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: selectorLabels,
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{},
|
||||||
|
},
|
||||||
|
Template: api.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: selectorLabels,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
RestartPolicy: api.RestartPolicyAlways,
|
||||||
|
DNSPolicy: api.DNSClusterFirst,
|
||||||
|
Containers: []api.Container{{Name: fakeImageName, Image: fakeImage, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ go_test(
|
||||||
"//pkg/controller/deployment/util:go_default_library",
|
"//pkg/controller/deployment/util:go_default_library",
|
||||||
"//test/integration/framework:go_default_library",
|
"//test/integration/framework:go_default_library",
|
||||||
"//vendor/k8s.io/api/core/v1:go_default_library",
|
"//vendor/k8s.io/api/core/v1:go_default_library",
|
||||||
|
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,12 @@ limitations under the License.
|
||||||
package deployment
|
package deployment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
|
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
|
@ -69,3 +72,65 @@ func TestNewDeployment(t *testing.T) {
|
||||||
t.Errorf("expected new ReplicaSet last-applied annotation not copied from Deployment %s", deploy.Name)
|
t.Errorf("expected new ReplicaSet last-applied annotation not copied from Deployment %s", deploy.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectors are IMMUTABLE for all API versions except apps/v1beta1 and extensions/v1beta1
|
||||||
|
func TestDeploymentSelectorImmutability(t *testing.T) {
|
||||||
|
s, closeFn, c := dcSimpleSetup(t)
|
||||||
|
defer closeFn()
|
||||||
|
name := "test-deployment-selector-immutability"
|
||||||
|
ns := framework.CreateTestingNamespace(name, s, t)
|
||||||
|
defer framework.DeleteTestingNamespace(ns, s, t)
|
||||||
|
|
||||||
|
tester := &deploymentTester{t: t, c: c, deployment: newDeployment(name, ns.Name, int32(20))}
|
||||||
|
deploymentExtensionsV1beta1, err := c.ExtensionsV1beta1().Deployments(ns.Name).Create(tester.deployment)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create extensions/v1beta1 deployment %s: %v", tester.deployment.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test to ensure extensions/v1beta1 selector is mutable
|
||||||
|
newSelectorLabels := map[string]string{"name_extensions_v1beta1": "test_extensions_v1beta1"}
|
||||||
|
deploymentExtensionsV1beta1.Spec.Selector.MatchLabels = newSelectorLabels
|
||||||
|
deploymentExtensionsV1beta1.Spec.Template.Labels = newSelectorLabels
|
||||||
|
updatedDeploymentExtensionsV1beta1, err := c.ExtensionsV1beta1().Deployments(ns.Name).Update(deploymentExtensionsV1beta1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update extensions/v1beta1 deployment %s: %v", deploymentExtensionsV1beta1.Name, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(updatedDeploymentExtensionsV1beta1.Spec.Selector.MatchLabels, newSelectorLabels) {
|
||||||
|
t.Errorf("selector should be changed for extensions/v1beta1, expected: %v, got: %v", newSelectorLabels, updatedDeploymentExtensionsV1beta1.Spec.Selector.MatchLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test to ensure apps/v1beta1 selector is mutable
|
||||||
|
deploymentAppsV1beta1, err := c.AppsV1beta1().Deployments(ns.Name).Get(updatedDeploymentExtensionsV1beta1.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get apps/v1beta1 deployment %s: %v", updatedDeploymentExtensionsV1beta1.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newSelectorLabels = map[string]string{"name_apps_v1beta1": "test_apps_v1beta1"}
|
||||||
|
deploymentAppsV1beta1.Spec.Selector.MatchLabels = newSelectorLabels
|
||||||
|
deploymentAppsV1beta1.Spec.Template.Labels = newSelectorLabels
|
||||||
|
updatedDeploymentAppsV1beta1, err := c.AppsV1beta1().Deployments(ns.Name).Update(deploymentAppsV1beta1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update apps/v1beta1 deployment %s: %v", deploymentAppsV1beta1.Name, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(updatedDeploymentAppsV1beta1.Spec.Selector.MatchLabels, newSelectorLabels) {
|
||||||
|
t.Errorf("selector should be changed for apps/v1beta1, expected: %v, got: %v", newSelectorLabels, updatedDeploymentAppsV1beta1.Spec.Selector.MatchLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test to ensure apps/v1beta2 selector is immutable
|
||||||
|
deploymentAppsV1beta2, err := c.AppsV1beta2().Deployments(ns.Name).Get(updatedDeploymentAppsV1beta1.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get apps/v1beta2 deployment %s: %v", updatedDeploymentAppsV1beta1.Name, err)
|
||||||
|
}
|
||||||
|
newSelectorLabels = map[string]string{"name_apps_v1beta2": "test_apps_v1beta2"}
|
||||||
|
deploymentAppsV1beta2.Spec.Selector.MatchLabels = newSelectorLabels
|
||||||
|
deploymentAppsV1beta2.Spec.Template.Labels = newSelectorLabels
|
||||||
|
_, err = c.AppsV1beta2().Deployments(ns.Name).Update(deploymentAppsV1beta2)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("failed to provide validation error when changing immutable selector when updating apps/v1beta2 deployment %s", deploymentAppsV1beta2.Name)
|
||||||
|
}
|
||||||
|
expectedErrType := "Invalid value"
|
||||||
|
expectedErrDetail := "field is immutable"
|
||||||
|
if !strings.Contains(err.Error(), expectedErrType) || !strings.Contains(err.Error(), expectedErrDetail) {
|
||||||
|
t.Errorf("error message does not match, expected type: %s, expected detail: %s, got: %s", expectedErrType, expectedErrDetail, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -115,6 +115,20 @@ func dcSetup(t *testing.T) (*httptest.Server, framework.CloseFunc, *replicaset.R
|
||||||
return s, closeFn, rm, dc, informers, clientSet
|
return s, closeFn, rm, dc, informers, clientSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dcSimpleSetup sets up necessities for Deployment integration test, including master, apiserver,
|
||||||
|
// and clientset, but not controllers and informers
|
||||||
|
func dcSimpleSetup(t *testing.T) (*httptest.Server, framework.CloseFunc, clientset.Interface) {
|
||||||
|
masterConfig := framework.NewIntegrationTestMasterConfig()
|
||||||
|
_, s, closeFn := framework.RunAMaster(masterConfig)
|
||||||
|
|
||||||
|
config := restclient.Config{Host: s.URL}
|
||||||
|
clientSet, err := clientset.NewForConfig(&config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error in create clientset: %v", err)
|
||||||
|
}
|
||||||
|
return s, closeFn, clientSet
|
||||||
|
}
|
||||||
|
|
||||||
// addPodConditionReady sets given pod status to ready at given time
|
// addPodConditionReady sets given pod status to ready at given time
|
||||||
func addPodConditionReady(pod *v1.Pod, time metav1.Time) {
|
func addPodConditionReady(pod *v1.Pod, time metav1.Time) {
|
||||||
pod.Status = v1.PodStatus{
|
pod.Status = v1.PodStatus{
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -149,6 +150,18 @@ func rmSetup(t *testing.T) (*httptest.Server, framework.CloseFunc, *replicaset.R
|
||||||
return s, closeFn, rm, informers, clientSet
|
return s, closeFn, rm, informers, clientSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rmSimpleSetup(t *testing.T) (*httptest.Server, framework.CloseFunc, clientset.Interface) {
|
||||||
|
masterConfig := framework.NewIntegrationTestMasterConfig()
|
||||||
|
_, s, closeFn := framework.RunAMaster(masterConfig)
|
||||||
|
|
||||||
|
config := restclient.Config{Host: s.URL}
|
||||||
|
clientSet, err := clientset.NewForConfig(&config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error in create clientset: %v", err)
|
||||||
|
}
|
||||||
|
return s, closeFn, clientSet
|
||||||
|
}
|
||||||
|
|
||||||
// wait for the podInformer to observe the pods. Call this function before
|
// wait for the podInformer to observe the pods. Call this function before
|
||||||
// running the RS controller to prevent the rc manager from creating new pods
|
// running the RS controller to prevent the rc manager from creating new pods
|
||||||
// rather than adopting the existing ones.
|
// rather than adopting the existing ones.
|
||||||
|
@ -462,3 +475,43 @@ func TestUpdateLabelToBeAdopted(t *testing.T) {
|
||||||
}
|
}
|
||||||
close(stopCh)
|
close(stopCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectors are IMMUTABLE for all API versions except extensions/v1beta1
|
||||||
|
func TestRSSelectorImmutability(t *testing.T) {
|
||||||
|
s, closeFn, clientSet := rmSimpleSetup(t)
|
||||||
|
defer closeFn()
|
||||||
|
ns := framework.CreateTestingNamespace("rs-selector-immutability", s, t)
|
||||||
|
defer framework.DeleteTestingNamespace(ns, s, t)
|
||||||
|
rs := newRS("rs", ns.Name, 0)
|
||||||
|
createRSsPods(t, clientSet, []*v1beta1.ReplicaSet{rs}, []*v1.Pod{}, ns.Name)
|
||||||
|
|
||||||
|
// test to ensure extensions/v1beta1 selector is mutable
|
||||||
|
newSelectorLabels := map[string]string{"changed_name_extensions_v1beta1": "changed_test_extensions_v1beta1"}
|
||||||
|
rs.Spec.Selector.MatchLabels = newSelectorLabels
|
||||||
|
rs.Spec.Template.Labels = newSelectorLabels
|
||||||
|
replicaset, err := clientSet.ExtensionsV1beta1().ReplicaSets(ns.Name).Update(rs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to update extensions/v1beta1 replicaset %s: %v", replicaset.Name, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(replicaset.Spec.Selector.MatchLabels, newSelectorLabels) {
|
||||||
|
t.Errorf("selector should be changed for extensions/v1beta1, expected: %v, got: %v", newSelectorLabels, replicaset.Spec.Selector.MatchLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test to ensure apps/v1beta2 selector is immutable
|
||||||
|
rsV1beta2, err := clientSet.AppsV1beta2().ReplicaSets(ns.Name).Get(replicaset.Name, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get apps/v1beta2 replicaset %s: %v", replicaset.Name, err)
|
||||||
|
}
|
||||||
|
newSelectorLabels = map[string]string{"changed_name_apps_v1beta2": "changed_test_apps_v1beta2"}
|
||||||
|
rsV1beta2.Spec.Selector.MatchLabels = newSelectorLabels
|
||||||
|
rsV1beta2.Spec.Template.Labels = newSelectorLabels
|
||||||
|
_, err = clientSet.AppsV1beta2().ReplicaSets(ns.Name).Update(rsV1beta2)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("failed to provide validation error when changing immutable selector when updating apps/v1beta2 replicaset %s", rsV1beta2.Name)
|
||||||
|
}
|
||||||
|
expectedErrType := "Invalid value"
|
||||||
|
expectedErrDetail := "field is immutable"
|
||||||
|
if !strings.Contains(err.Error(), expectedErrType) || !strings.Contains(err.Error(), expectedErrDetail) {
|
||||||
|
t.Errorf("error message does not match, expected type: %s, expected detail: %s, got: %s", expectedErrType, expectedErrDetail, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue