From 0bcab9ab114291ad8d7367fb5cf9da804649d016 Mon Sep 17 00:00:00 2001 From: "Madhusudan.C.S" Date: Mon, 18 Jan 2016 12:32:44 -0800 Subject: [PATCH] Implement ReplicaSet registry. --- pkg/api/unversioned/helpers.go | 16 ++ pkg/registry/cachesize/cachesize.go | 2 + pkg/registry/controller/etcd/etcd.go | 2 + pkg/registry/controller/registry.go | 2 + pkg/registry/controller/strategy.go | 2 + pkg/registry/replicaset/doc.go | 19 ++ pkg/registry/replicaset/etcd/etcd.go | 112 ++++++++++ pkg/registry/replicaset/etcd/etcd_test.go | 237 ++++++++++++++++++++++ pkg/registry/replicaset/registry.go | 93 +++++++++ pkg/registry/replicaset/strategy.go | 149 ++++++++++++++ pkg/registry/replicaset/strategy_test.go | 142 +++++++++++++ 11 files changed, 776 insertions(+) create mode 100644 pkg/registry/replicaset/doc.go create mode 100644 pkg/registry/replicaset/etcd/etcd.go create mode 100644 pkg/registry/replicaset/etcd/etcd_test.go create mode 100644 pkg/registry/replicaset/registry.go create mode 100644 pkg/registry/replicaset/strategy.go create mode 100644 pkg/registry/replicaset/strategy_test.go diff --git a/pkg/api/unversioned/helpers.go b/pkg/api/unversioned/helpers.go index 3a3d7ee893c..c8451a96bbe 100644 --- a/pkg/api/unversioned/helpers.go +++ b/pkg/api/unversioned/helpers.go @@ -62,3 +62,19 @@ func LabelSelectorAsSelector(ps *LabelSelector) (labels.Selector, error) { } return selector, nil } + +// SetAsLabelSelector converts the labels.Set object into a LabelSelector api object. +func SetAsLabelSelector(ls labels.Set) *LabelSelector { + if ls == nil { + return nil + } + + selector := &LabelSelector{ + MatchLabels: make(map[string]string), + } + for label, value := range ls { + selector.MatchLabels[label] = value + } + + return selector +} diff --git a/pkg/registry/cachesize/cachesize.go b/pkg/registry/cachesize/cachesize.go index 2261ba09fed..708a4441d21 100644 --- a/pkg/registry/cachesize/cachesize.go +++ b/pkg/registry/cachesize/cachesize.go @@ -42,6 +42,7 @@ const ( PersistentVolumeClaims Resource = "persistentvolumeclaims" Pods Resource = "pods" PodTemplates Resource = "podtemplates" + Replicasets Resource = "replicasets" ResourceQuotas Resource = "resourcequotas" Secrets Resource = "secrets" ServiceAccounts Resource = "serviceaccounts" @@ -66,6 +67,7 @@ func init() { watchCacheSizes[PersistentVolumeClaims] = 100 watchCacheSizes[Pods] = 1000 watchCacheSizes[PodTemplates] = 100 + watchCacheSizes[Replicasets] = 100 watchCacheSizes[ResourceQuotas] = 100 watchCacheSizes[Secrets] = 100 watchCacheSizes[ServiceAccounts] = 100 diff --git a/pkg/registry/controller/etcd/etcd.go b/pkg/registry/controller/etcd/etcd.go index 38ed7e23452..92c6c813996 100644 --- a/pkg/registry/controller/etcd/etcd.go +++ b/pkg/registry/controller/etcd/etcd.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// If you make changes to this file, you should also make the corresponding change in ReplicaSet. + package etcd import ( diff --git a/pkg/registry/controller/registry.go b/pkg/registry/controller/registry.go index 12aa757405c..e3ecfa6a66c 100644 --- a/pkg/registry/controller/registry.go +++ b/pkg/registry/controller/registry.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// If you make changes to this file, you should also make the corresponding change in ReplicaSet. + package controller import ( diff --git a/pkg/registry/controller/strategy.go b/pkg/registry/controller/strategy.go index 0ef9fba00b5..2017672ef70 100644 --- a/pkg/registry/controller/strategy.go +++ b/pkg/registry/controller/strategy.go @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// If you make changes to this file, you should also make the corresponding change in ReplicaSet. + package controller import ( diff --git a/pkg/registry/replicaset/doc.go b/pkg/registry/replicaset/doc.go new file mode 100644 index 00000000000..ee349fae740 --- /dev/null +++ b/pkg/registry/replicaset/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package replicaset provides Registry interface and it's RESTStorage +// implementation for storing ReplicaSet api objects. +package replicaset diff --git a/pkg/registry/replicaset/etcd/etcd.go b/pkg/registry/replicaset/etcd/etcd.go new file mode 100644 index 00000000000..f6d606be2a3 --- /dev/null +++ b/pkg/registry/replicaset/etcd/etcd.go @@ -0,0 +1,112 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// If you make changes to this file, you should also make the corresponding change in ReplicationController. + +package etcd + +import ( + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/cachesize" + "k8s.io/kubernetes/pkg/registry/generic" + etcdgeneric "k8s.io/kubernetes/pkg/registry/generic/etcd" + "k8s.io/kubernetes/pkg/registry/replicaset" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/storage" +) + +// ReplicaSetStorage includes dummy storage for ReplicaSets and for Scale subresource. +type ReplicaSetStorage struct { + ReplicaSet *REST + Status *StatusREST +} + +func NewStorage(s storage.Interface, storageDecorator generic.StorageDecorator) ReplicaSetStorage { + replicaSetRest, replicaSetStatusRest := NewREST(s, storageDecorator) + + return ReplicaSetStorage{ + ReplicaSet: replicaSetRest, + Status: replicaSetStatusRest, + } +} + +type REST struct { + *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against ReplicaSet. +func NewREST(s storage.Interface, storageDecorator generic.StorageDecorator) (*REST, *StatusREST) { + prefix := "/replicasets" + + newListFunc := func() runtime.Object { return &extensions.ReplicaSetList{} } + storageInterface := storageDecorator( + s, cachesize.GetWatchCacheSizeByResource(cachesize.Replicasets), &extensions.ReplicaSet{}, prefix, replicaset.Strategy, newListFunc) + + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &extensions.ReplicaSet{} }, + + // NewListFunc returns an object capable of storing results of an etcd list. + NewListFunc: newListFunc, + // Produces a path that etcd understands, to the root of the resource + // by combining the namespace in the context with the given prefix + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + // Produces a path that etcd understands, to the resource by combining + // the namespace in the context with the given prefix + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name) + }, + // Retrieve the name field of a ReplicaSet + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*extensions.ReplicaSet).Name, nil + }, + // Used to match objects based on labels/fields for list and watch + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return replicaset.MatchReplicaSet(label, field) + }, + QualifiedResource: api.Resource("replicasets"), + + // Used to validate ReplicaSet creation + CreateStrategy: replicaset.Strategy, + + // Used to validate ReplicaSet updates + UpdateStrategy: replicaset.Strategy, + + Storage: storageInterface, + } + statusStore := *store + statusStore.UpdateStrategy = replicaset.StatusStrategy + + return &REST{store}, &StatusREST{store: &statusStore} +} + +// StatusREST implements the REST endpoint for changing the status of a ReplicaSet +type StatusREST struct { + store *etcdgeneric.Etcd +} + +func (r *StatusREST) New() runtime.Object { + return &extensions.ReplicaSet{} +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) +} diff --git a/pkg/registry/replicaset/etcd/etcd_test.go b/pkg/registry/replicaset/etcd/etcd_test.go new file mode 100644 index 00000000000..f7654597514 --- /dev/null +++ b/pkg/registry/replicaset/etcd/etcd_test.go @@ -0,0 +1,237 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/registry/registrytest" + "k8s.io/kubernetes/pkg/runtime" + etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing" +) + +func newStorage(t *testing.T) (*ReplicaSetStorage, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, "extensions") + replicaSetStorage := NewStorage(etcdStorage, generic.UndecoratedStorage) + return &replicaSetStorage, server +} + +// createReplicaSet is a helper function that returns a ReplicaSet with the updated resource version. +func createReplicaSet(storage *REST, rs extensions.ReplicaSet, t *testing.T) (extensions.ReplicaSet, error) { + ctx := api.WithNamespace(api.NewContext(), rs.Namespace) + obj, err := storage.Create(ctx, &rs) + if err != nil { + t.Errorf("Failed to create ReplicaSet, %v", err) + } + newRS := obj.(*extensions.ReplicaSet) + return *newRS, nil +} + +func validNewReplicaSet() *extensions.ReplicaSet { + return &extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: extensions.ReplicaSetSpec{ + Selector: &unversioned.LabelSelector{MatchLabels: map[string]string{"a": "b"}}, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: map[string]string{"a": "b"}, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: "test", + Image: "test_image", + ImagePullPolicy: api.PullIfNotPresent, + }, + }, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + }, + }, + Replicas: 7, + }, + Status: extensions.ReplicaSetStatus{ + Replicas: 5, + }, + } +} + +var validReplicaSet = *validNewReplicaSet() + +func TestCreate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + rs := validNewReplicaSet() + rs.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + rs, + // invalid (invalid selector) + &extensions.ReplicaSet{ + Spec: extensions.ReplicaSetSpec{ + Replicas: 2, + Selector: &unversioned.LabelSelector{MatchLabels: map[string]string{}}, + Template: validReplicaSet.Spec.Template, + }, + }, + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + test.TestUpdate( + // valid + validNewReplicaSet(), + // valid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*extensions.ReplicaSet) + object.Spec.Replicas = object.Spec.Replicas + 1 + return object + }, + // invalid updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*extensions.ReplicaSet) + object.UID = "newUID" + return object + }, + func(obj runtime.Object) runtime.Object { + object := obj.(*extensions.ReplicaSet) + object.Name = "" + return object + }, + func(obj runtime.Object) runtime.Object { + object := obj.(*extensions.ReplicaSet) + object.Spec.Selector = &unversioned.LabelSelector{MatchLabels: map[string]string{}} + return object + }, + ) +} + +func TestDelete(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + test.TestDelete(validNewReplicaSet()) +} + +func TestGenerationNumber(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + modifiedSno := *validNewReplicaSet() + modifiedSno.Generation = 100 + modifiedSno.Status.ObservedGeneration = 10 + ctx := api.NewDefaultContext() + rs, err := createReplicaSet(storage.ReplicaSet, modifiedSno, t) + etcdRS, err := storage.ReplicaSet.Get(ctx, rs.Name) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + storedRS, _ := etcdRS.(*extensions.ReplicaSet) + + // Generation initialization + if storedRS.Generation != 1 && storedRS.Status.ObservedGeneration != 0 { + t.Fatalf("Unexpected generation number %v, status generation %v", storedRS.Generation, storedRS.Status.ObservedGeneration) + } + + // Updates to spec should increment the generation number + storedRS.Spec.Replicas += 1 + storage.ReplicaSet.Update(ctx, storedRS) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + etcdRS, err = storage.ReplicaSet.Get(ctx, rs.Name) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + storedRS, _ = etcdRS.(*extensions.ReplicaSet) + if storedRS.Generation != 2 || storedRS.Status.ObservedGeneration != 0 { + t.Fatalf("Unexpected generation, spec: %v, status: %v", storedRS.Generation, storedRS.Status.ObservedGeneration) + } + + // Updates to status should not increment either spec or status generation numbers + storedRS.Status.Replicas += 1 + storage.ReplicaSet.Update(ctx, storedRS) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + etcdRS, err = storage.ReplicaSet.Get(ctx, rs.Name) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + storedRS, _ = etcdRS.(*extensions.ReplicaSet) + if storedRS.Generation != 2 || storedRS.Status.ObservedGeneration != 0 { + t.Fatalf("Unexpected generation number, spec: %v, status: %v", storedRS.Generation, storedRS.Status.ObservedGeneration) + } +} + +func TestGet(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + test.TestGet(validNewReplicaSet()) +} + +func TestList(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + test.TestList(validNewReplicaSet()) +} + +func TestWatch(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.ReplicaSet.Etcd) + test.TestWatch( + validNewReplicaSet(), + // matching labels + []labels.Set{ + {"a": "b"}, + }, + // not matching labels + []labels.Set{ + {"a": "c"}, + {"foo": "bar"}, + }, + // matching fields + []fields.Set{ + {"status.replicas": "5"}, + {"metadata.name": "foo"}, + {"status.replicas": "5", "metadata.name": "foo"}, + }, + // not matchin fields + []fields.Set{ + {"status.replicas": "10"}, + {"metadata.name": "bar"}, + {"name": "foo"}, + {"status.replicas": "10", "metadata.name": "foo"}, + {"status.replicas": "0", "metadata.name": "bar"}, + }, + ) +} diff --git a/pkg/registry/replicaset/registry.go b/pkg/registry/replicaset/registry.go new file mode 100644 index 00000000000..d1279179a32 --- /dev/null +++ b/pkg/registry/replicaset/registry.go @@ -0,0 +1,93 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// If you make changes to this file, you should also make the corresponding change in ReplicationController. + +package replicaset + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/watch" +) + +// Registry is an interface for things that know how to store ReplicaSets. +type Registry interface { + ListReplicaSets(ctx api.Context, options *api.ListOptions) (*extensions.ReplicaSetList, error) + WatchReplicaSets(ctx api.Context, options *api.ListOptions) (watch.Interface, error) + GetReplicaSet(ctx api.Context, replicaSetID string) (*extensions.ReplicaSet, error) + CreateReplicaSet(ctx api.Context, replicaSet *extensions.ReplicaSet) (*extensions.ReplicaSet, error) + UpdateReplicaSet(ctx api.Context, replicaSet *extensions.ReplicaSet) (*extensions.ReplicaSet, error) + DeleteReplicaSet(ctx api.Context, replicaSetID string) error +} + +// storage puts strong typing around storage calls +type storage struct { + rest.StandardStorage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s rest.StandardStorage) Registry { + return &storage{s} +} + +func (s *storage) ListReplicaSets(ctx api.Context, options *api.ListOptions) (*extensions.ReplicaSetList, error) { + if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() { + return nil, fmt.Errorf("field selector not supported yet") + } + obj, err := s.List(ctx, options) + if err != nil { + return nil, err + } + return obj.(*extensions.ReplicaSetList), err +} + +func (s *storage) WatchReplicaSets(ctx api.Context, options *api.ListOptions) (watch.Interface, error) { + return s.Watch(ctx, options) +} + +func (s *storage) GetReplicaSet(ctx api.Context, replicaSetID string) (*extensions.ReplicaSet, error) { + obj, err := s.Get(ctx, replicaSetID) + if err != nil { + return nil, err + } + return obj.(*extensions.ReplicaSet), nil +} + +func (s *storage) CreateReplicaSet(ctx api.Context, replicaSet *extensions.ReplicaSet) (*extensions.ReplicaSet, error) { + obj, err := s.Create(ctx, replicaSet) + if err != nil { + return nil, err + } + return obj.(*extensions.ReplicaSet), nil +} + +func (s *storage) UpdateReplicaSet(ctx api.Context, replicaSet *extensions.ReplicaSet) (*extensions.ReplicaSet, error) { + obj, _, err := s.Update(ctx, replicaSet) + if err != nil { + return nil, err + } + return obj.(*extensions.ReplicaSet), nil +} + +func (s *storage) DeleteReplicaSet(ctx api.Context, replicaSetID string) error { + _, err := s.Delete(ctx, replicaSetID, nil) + return err +} diff --git a/pkg/registry/replicaset/strategy.go b/pkg/registry/replicaset/strategy.go new file mode 100644 index 00000000000..71da18cdb02 --- /dev/null +++ b/pkg/registry/replicaset/strategy.go @@ -0,0 +1,149 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// If you make changes to this file, you should also make the corresponding change in ReplicationController. + +package replicaset + +import ( + "fmt" + "reflect" + "strconv" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/apis/extensions/validation" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/validation/field" +) + +// rsStrategy implements verification logic for ReplicaSets. +type rsStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating ReplicaSet objects. +var Strategy = rsStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped returns true because all ReplicaSets need to be within a namespace. +func (rsStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate clears the status of a ReplicaSet before creation. +func (rsStrategy) PrepareForCreate(obj runtime.Object) { + rs := obj.(*extensions.ReplicaSet) + rs.Status = extensions.ReplicaSetStatus{} + + rs.Generation = 1 +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (rsStrategy) PrepareForUpdate(obj, old runtime.Object) { + newRS := obj.(*extensions.ReplicaSet) + oldRS := old.(*extensions.ReplicaSet) + // update is not allowed to set status + newRS.Status = oldRS.Status + + // Any changes to the spec increment the generation number, any changes to the + // status should reflect the generation number of the corresponding object. We push + // the burden of managing the status onto the clients because we can't (in general) + // know here what version of spec the writer of the status has seen. It may seem like + // we can at first -- since obj contains spec -- but in the future we will probably make + // status its own object, and even if we don't, writes may be the result of a + // read-update-write loop, so the contents of spec may not actually be the spec that + // the ReplicaSet has *seen*. + // + // TODO: Any changes to a part of the object that represents desired state (labels, + // annotations etc) should also increment the generation. + if !reflect.DeepEqual(oldRS.Spec, newRS.Spec) { + newRS.Generation = oldRS.Generation + 1 + } +} + +// Validate validates a new ReplicaSet. +func (rsStrategy) Validate(ctx api.Context, obj runtime.Object) field.ErrorList { + rs := obj.(*extensions.ReplicaSet) + return validation.ValidateReplicaSet(rs) +} + +// Canonicalize normalizes the object after validation. +func (rsStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for ReplicaSets; this means a POST is +// needed to create one. +func (rsStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (rsStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) field.ErrorList { + validationErrorList := validation.ValidateReplicaSet(obj.(*extensions.ReplicaSet)) + updateErrorList := validation.ValidateReplicaSetUpdate(obj.(*extensions.ReplicaSet), old.(*extensions.ReplicaSet)) + return append(validationErrorList, updateErrorList...) +} + +func (rsStrategy) AllowUnconditionalUpdate() bool { + return true +} + +// ReplicaSetToSelectableFields returns a field set that represents the object. +func ReplicaSetToSelectableFields(rs *extensions.ReplicaSet) fields.Set { + objectMetaFieldsSet := generic.ObjectMetaFieldsSet(rs.ObjectMeta, true) + rsSpecificFieldsSet := fields.Set{ + "status.replicas": strconv.Itoa(rs.Status.Replicas), + } + return generic.MergeFieldsSets(objectMetaFieldsSet, rsSpecificFieldsSet) +} + +// MatchReplicaSet is the filter used by the generic etcd backend to route +// watch events from etcd to clients of the apiserver only interested in specific +// labels/fields. +func MatchReplicaSet(label labels.Selector, field fields.Selector) generic.Matcher { + return &generic.SelectionPredicate{ + Label: label, + Field: field, + GetAttrs: func(obj runtime.Object) (labels.Set, fields.Set, error) { + rs, ok := obj.(*extensions.ReplicaSet) + if !ok { + return nil, nil, fmt.Errorf("Given object is not a ReplicaSet.") + } + return labels.Set(rs.ObjectMeta.Labels), ReplicaSetToSelectableFields(rs), nil + }, + } +} + +type rsStatusStrategy struct { + rsStrategy +} + +var StatusStrategy = rsStatusStrategy{Strategy} + +func (rsStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + newRS := obj.(*extensions.ReplicaSet) + oldRS := old.(*extensions.ReplicaSet) + // update is not allowed to set spec + newRS.Spec = oldRS.Spec +} + +func (rsStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateReplicaSetStatusUpdate(obj.(*extensions.ReplicaSet), old.(*extensions.ReplicaSet)) +} diff --git a/pkg/registry/replicaset/strategy_test.go b/pkg/registry/replicaset/strategy_test.go new file mode 100644 index 00000000000..1fe9b9f3484 --- /dev/null +++ b/pkg/registry/replicaset/strategy_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package replicaset + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" +) + +func TestReplicaSetStrategy(t *testing.T) { + ctx := api.NewDefaultContext() + if !Strategy.NamespaceScoped() { + t.Errorf("ReplicaSet must be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("ReplicaSet should not allow create on update") + } + + validSelector := map[string]string{"a": "b"} + validPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + } + rs := &extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: extensions.ReplicaSetSpec{ + Selector: &unversioned.LabelSelector{MatchLabels: validSelector}, + Template: &validPodTemplate.Template, + }, + Status: extensions.ReplicaSetStatus{ + Replicas: 1, + ObservedGeneration: int64(10), + }, + } + + Strategy.PrepareForCreate(rs) + if rs.Status.Replicas != 0 { + t.Error("ReplicaSet should not allow setting status.replicas on create") + } + if rs.Status.ObservedGeneration != int64(0) { + t.Error("ReplicaSet should not allow setting status.observedGeneration on create") + } + errs := Strategy.Validate(ctx, rs) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + + invalidRc := &extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{Name: "bar", ResourceVersion: "4"}, + } + Strategy.PrepareForUpdate(invalidRc, rs) + errs = Strategy.ValidateUpdate(ctx, invalidRc, rs) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + if invalidRc.ResourceVersion != "4" { + t.Errorf("Incoming resource version on update should not be mutated") + } +} + +func TestReplicaSetStatusStrategy(t *testing.T) { + ctx := api.NewDefaultContext() + if !StatusStrategy.NamespaceScoped() { + t.Errorf("ReplicaSet must be namespace scoped") + } + if StatusStrategy.AllowCreateOnUpdate() { + t.Errorf("ReplicaSet should not allow create on update") + } + validSelector := map[string]string{"a": "b"} + validPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + } + oldRS := &extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault, ResourceVersion: "10"}, + Spec: extensions.ReplicaSetSpec{ + Replicas: 3, + Selector: &unversioned.LabelSelector{MatchLabels: validSelector}, + Template: &validPodTemplate.Template, + }, + Status: extensions.ReplicaSetStatus{ + Replicas: 1, + ObservedGeneration: int64(10), + }, + } + newRS := &extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault, ResourceVersion: "9"}, + Spec: extensions.ReplicaSetSpec{ + Replicas: 1, + Selector: &unversioned.LabelSelector{MatchLabels: validSelector}, + Template: &validPodTemplate.Template, + }, + Status: extensions.ReplicaSetStatus{ + Replicas: 3, + ObservedGeneration: int64(11), + }, + } + StatusStrategy.PrepareForUpdate(newRS, oldRS) + if newRS.Status.Replicas != 3 { + t.Errorf("ReplicaSet status updates should allow change of replicas: %v", newRS.Status.Replicas) + } + if newRS.Spec.Replicas != 3 { + t.Errorf("PrepareForUpdate should have preferred spec") + } + errs := StatusStrategy.ValidateUpdate(ctx, newRS, oldRS) + if len(errs) != 0 { + t.Errorf("Unexpected error %v", errs) + } +}