From 1c1ad1ad3f03700d9e60aacd31d1ff5ada0c227f Mon Sep 17 00:00:00 2001 From: jianhuiz Date: Thu, 31 Mar 2016 17:16:25 -0700 Subject: [PATCH] add cluster registry --- federation/registry/cluster/etcd/etcd.go | 82 +++++++++ federation/registry/cluster/etcd/etcd_test.go | 140 ++++++++++++++++ federation/registry/cluster/registry.go | 81 +++++++++ federation/registry/cluster/strategy.go | 119 +++++++++++++ federation/registry/cluster/strategy_test.go | 157 ++++++++++++++++++ hack/test-go.sh | 2 +- pkg/api/testapi/testapi.go | 11 ++ 7 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 federation/registry/cluster/etcd/etcd.go create mode 100644 federation/registry/cluster/etcd/etcd_test.go create mode 100644 federation/registry/cluster/registry.go create mode 100644 federation/registry/cluster/strategy.go create mode 100644 federation/registry/cluster/strategy_test.go diff --git a/federation/registry/cluster/etcd/etcd.go b/federation/registry/cluster/etcd/etcd.go new file mode 100644 index 00000000000..6b4cd179bc1 --- /dev/null +++ b/federation/registry/cluster/etcd/etcd.go @@ -0,0 +1,82 @@ +/* +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 ( + "k8s.io/kubernetes/federation/apis/federation" + "k8s.io/kubernetes/federation/registry/cluster" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/registry/generic" + "k8s.io/kubernetes/pkg/registry/generic/registry" + "k8s.io/kubernetes/pkg/runtime" +) + +type REST struct { + *registry.Store +} + +type StatusREST struct { + store *registry.Store +} + +func (r *StatusREST) New() runtime.Object { + return &federation.Cluster{} +} + +// 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) +} + +// NewREST returns a RESTStorage object that will work against clusters. +func NewREST(opts generic.RESTOptions) (*REST, *StatusREST) { + prefix := "/clusters" + + newListFunc := func() runtime.Object { return &federation.ClusterList{} } + storageInterface := opts.Decorator( + opts.Storage, 100, &federation.Cluster{}, prefix, cluster.Strategy, newListFunc) + + store := ®istry.Store{ + NewFunc: func() runtime.Object { return &federation.Cluster{} }, + NewListFunc: newListFunc, + KeyRootFunc: func(ctx api.Context) string { + return prefix + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return registry.NoNamespaceKeyFunc(ctx, prefix, name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*federation.Cluster).Name, nil + }, + PredicateFunc: cluster.MatchCluster, + QualifiedResource: federation.Resource("clusters"), + DeleteCollectionWorkers: opts.DeleteCollectionWorkers, + + CreateStrategy: cluster.Strategy, + UpdateStrategy: cluster.Strategy, + DeleteStrategy: cluster.Strategy, + + ReturnDeletedObject: true, + + Storage: storageInterface, + } + + statusStore := *store + statusStore.UpdateStrategy = cluster.StatusStrategy + + return &REST{store}, &StatusREST{store: &statusStore} +} diff --git a/federation/registry/cluster/etcd/etcd_test.go b/federation/registry/cluster/etcd/etcd_test.go new file mode 100644 index 00000000000..cb4d3278767 --- /dev/null +++ b/federation/registry/cluster/etcd/etcd_test.go @@ -0,0 +1,140 @@ +/* +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/federation/apis/federation" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "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) (*REST, *etcdtesting.EtcdTestServer) { + etcdStorage, server := registrytest.NewEtcdStorage(t, federation.GroupName) + restOptions := generic.RESTOptions{ + Storage: etcdStorage, + Decorator: generic.UndecoratedStorage, + DeleteCollectionWorkers: 1} + storage, _ := NewREST(restOptions) + return storage, server +} + +func validNewCluster() *federation.Cluster { + return &federation.Cluster{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "name": "foo", + }, + }, + Spec: federation.ClusterSpec{ + ServerAddressByClientCIDRs: []unversioned.ServerAddressByClientCIDR{ + { + ClientCIDR: "0.0.0.0/0", + ServerAddress: "localhost:8888", + }, + }, + }, + Status: federation.ClusterStatus{ + Phase: federation.ClusterPending, + }, + } +} + +func TestCreate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope() + cluster := validNewCluster() + cluster.ObjectMeta = api.ObjectMeta{GenerateName: "foo"} + test.TestCreate( + cluster, + &federation.Cluster{ + ObjectMeta: api.ObjectMeta{Name: "-a123-a_"}, + }, + ) +} + +func TestUpdate(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestUpdate( + // valid + validNewCluster(), + // updateFunc + func(obj runtime.Object) runtime.Object { + object := obj.(*federation.Cluster) + object.Spec.Credential = "bar" + return object + }, + ) +} + +func TestDelete(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope().ReturnDeletedObject() + test.TestDelete(validNewCluster()) +} + +func TestGet(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestGet(validNewCluster()) +} + +func TestList(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestList(validNewCluster()) +} + +func TestWatch(t *testing.T) { + storage, server := newStorage(t) + defer server.Terminate(t) + test := registrytest.New(t, storage.Store).ClusterScope() + test.TestWatch( + validNewCluster(), + // matching labels + []labels.Set{ + {"name": "foo"}, + }, + // not matching labels + []labels.Set{ + {"name": "bar"}, + {"foo": "bar"}, + }, + // matching fields + []fields.Set{ + {"metadata.name": "foo"}, + }, + // not matching fields + []fields.Set{ + {"metadata.name": "bar"}, + }, + ) +} diff --git a/federation/registry/cluster/registry.go b/federation/registry/cluster/registry.go new file mode 100644 index 00000000000..361ca91d14d --- /dev/null +++ b/federation/registry/cluster/registry.go @@ -0,0 +1,81 @@ +/* +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 cluster + +import ( + "k8s.io/kubernetes/federation/apis/federation" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/watch" +) + +// Registry is an interface implemented by things that know how to store Cluster objects. +type Registry interface { + ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error) + WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error) + GetCluster(ctx api.Context, name string) (*federation.Cluster, error) + CreateCluster(ctx api.Context, cluster *federation.Cluster) error + UpdateCluster(ctx api.Context, cluster *federation.Cluster) error + DeleteCluster(ctx api.Context, name 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) ListClusters(ctx api.Context, options *api.ListOptions) (*federation.ClusterList, error) { + obj, err := s.List(ctx, options) + if err != nil { + return nil, err + } + return obj.(*federation.ClusterList), nil +} + +func (s *storage) WatchCluster(ctx api.Context, options *api.ListOptions) (watch.Interface, error) { + return s.Watch(ctx, options) +} + +func (s *storage) GetCluster(ctx api.Context, name string) (*federation.Cluster, error) { + obj, err := s.Get(ctx, name) + if err != nil { + return nil, err + } + return obj.(*federation.Cluster), nil +} + +func (s *storage) CreateCluster(ctx api.Context, cluster *federation.Cluster) error { + _, err := s.Create(ctx, cluster) + return err +} + +func (s *storage) UpdateCluster(ctx api.Context, cluster *federation.Cluster) error { + _, _, err := s.Update(ctx, cluster) + return err +} + +func (s *storage) DeleteCluster(ctx api.Context, name string) error { + _, err := s.Delete(ctx, name, nil) + return err +} diff --git a/federation/registry/cluster/strategy.go b/federation/registry/cluster/strategy.go new file mode 100644 index 00000000000..1130a5e9653 --- /dev/null +++ b/federation/registry/cluster/strategy.go @@ -0,0 +1,119 @@ +/* +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 cluster + +import ( + "fmt" + + "k8s.io/kubernetes/federation/apis/federation" + "k8s.io/kubernetes/federation/apis/federation/validation" + "k8s.io/kubernetes/pkg/api" + "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" +) + +type clusterStrategy struct { + runtime.ObjectTyper + api.NameGenerator +} + +var Strategy = clusterStrategy{api.Scheme, api.SimpleNameGenerator} + +func (clusterStrategy) NamespaceScoped() bool { + return false +} + +func ClusterToSelectableFields(cluster *federation.Cluster) fields.Set { + objectMetaFieldsSet := generic.ObjectMetaFieldsSet(cluster.ObjectMeta, false) + specificFieldsSet := fields.Set{ + "status.phase": string(cluster.Status.Phase), + } + return generic.MergeFieldsSets(objectMetaFieldsSet, specificFieldsSet) +} + +func MatchCluster(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) { + cluster, ok := obj.(*federation.Cluster) + if !ok { + return nil, nil, fmt.Errorf("given object is not a cluster.") + } + return labels.Set(cluster.ObjectMeta.Labels), ClusterToSelectableFields(cluster), nil + }, + } +} + +// PrepareForCreate clears fields that are not allowed to be set by end users on creation. +func (clusterStrategy) PrepareForCreate(obj runtime.Object) { + cluster := obj.(*federation.Cluster) + cluster.Status = federation.ClusterStatus{} +} + +// Validate validates a new cluster. +func (clusterStrategy) Validate(ctx api.Context, obj runtime.Object) field.ErrorList { + cluster := obj.(*federation.Cluster) + return validation.ValidateCluster(cluster) +} + +// Canonicalize normalizes the object after validation. +func (clusterStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is false for cluster. +func (clusterStrategy) AllowCreateOnUpdate() bool { + return false +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (clusterStrategy) PrepareForUpdate(obj, old runtime.Object) { + cluster := obj.(*federation.Cluster) + oldCluster := old.(*federation.Cluster) + cluster.Status = oldCluster.Status +} + +// ValidateUpdate is the default update validation for an end user. +func (clusterStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateClusterUpdate(obj.(*federation.Cluster), old.(*federation.Cluster)) +} +func (clusterStrategy) AllowUnconditionalUpdate() bool { + return true +} + +type clusterStatusStrategy struct { + clusterStrategy +} + +var StatusStrategy = clusterStatusStrategy{Strategy} + +func (clusterStatusStrategy) PrepareForCreate(obj runtime.Object) { + _ = obj.(*federation.Cluster) +} +func (clusterStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + cluster := obj.(*federation.Cluster) + oldCluster := old.(*federation.Cluster) + cluster.Spec = oldCluster.Spec +} + +// ValidateUpdate is the default update validation for an end user. +func (clusterStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateClusterStatusUpdate(obj.(*federation.Cluster), old.(*federation.Cluster)) +} diff --git a/federation/registry/cluster/strategy_test.go b/federation/registry/cluster/strategy_test.go new file mode 100644 index 00000000000..ccf9a94252d --- /dev/null +++ b/federation/registry/cluster/strategy_test.go @@ -0,0 +1,157 @@ +/* +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 cluster + +import ( + "testing" + + "k8s.io/kubernetes/federation/apis/federation" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + apitesting "k8s.io/kubernetes/pkg/api/testing" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" + "reflect" +) + +func validNewCluster() *federation.Cluster { + return &federation.Cluster{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + ResourceVersion: "4", + Labels: map[string]string{ + "name": "foo", + }, + }, + Spec: federation.ClusterSpec{ + ServerAddressByClientCIDRs: []unversioned.ServerAddressByClientCIDR{ + { + ClientCIDR: "0.0.0.0/0", + ServerAddress: "localhost:8888", + }, + }, + }, + Status: federation.ClusterStatus{ + Phase: federation.ClusterTerminated, + }, + } +} + +func invalidNewCluster() *federation.Cluster { + return &federation.Cluster{ + ObjectMeta: api.ObjectMeta{ + Name: "foo2", + ResourceVersion: "5", + }, + Spec: federation.ClusterSpec{ + Credential: "bar", + }, + Status: federation.ClusterStatus{ + Phase: federation.ClusterPending, + }, + } +} + +func TestClusterStrategy(t *testing.T) { + ctx := api.NewDefaultContext() + if Strategy.NamespaceScoped() { + t.Errorf("Cluster should not be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("Cluster should not allow create on update") + } + + cluster := validNewCluster() + Strategy.PrepareForCreate(cluster) + if len(cluster.Status.Phase) != 0 { + t.Errorf("Cluster should not allow setting phase on create") + } + errs := Strategy.Validate(ctx, cluster) + if len(errs) != 0 { + t.Errorf("Unexpected error validating %v", errs) + } + + invalidCluster := invalidNewCluster() + Strategy.PrepareForUpdate(invalidCluster, cluster) + if reflect.DeepEqual(invalidCluster.Spec, cluster.Spec) || + !reflect.DeepEqual(invalidCluster.Status, cluster.Status) { + t.Error("Only spec is expected being changed") + } + errs = Strategy.ValidateUpdate(ctx, invalidCluster, cluster) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + if cluster.ResourceVersion != "4" { + t.Errorf("Incoming resource version on update should not be mutated") + } +} + +func TestClusterStatusStrategy(t *testing.T) { + ctx := api.NewDefaultContext() + if StatusStrategy.NamespaceScoped() { + t.Errorf("Cluster should not be namespace scoped") + } + if StatusStrategy.AllowCreateOnUpdate() { + t.Errorf("Cluster should not allow create on update") + } + + cluster := validNewCluster() + invalidCluster := invalidNewCluster() + StatusStrategy.PrepareForUpdate(cluster, invalidCluster) + if !reflect.DeepEqual(invalidCluster.Spec, cluster.Spec) || + reflect.DeepEqual(invalidCluster.Status, cluster.Status) { + t.Error("Only spec is expected being changed") + } + errs := Strategy.ValidateUpdate(ctx, invalidCluster, cluster) + if len(errs) == 0 { + t.Errorf("Expected a validation error") + } + if cluster.ResourceVersion != "4" { + t.Errorf("Incoming resource version on update should not be mutated") + } +} + +func TestMatchCluster(t *testing.T) { + testFieldMap := map[bool][]fields.Set{ + true: { + {"metadata.name": "foo"}, + }, + false: { + {"foo": "bar"}, + }, + } + + for expectedResult, fieldSet := range testFieldMap { + for _, field := range fieldSet { + m := MatchCluster(labels.Everything(), field.AsSelector()) + _, matchesSingle := m.MatchesSingle() + if e, a := expectedResult, matchesSingle; e != a { + t.Errorf("%+v: expected %v, got %v", fieldSet, e, a) + } + } + } +} + +func TestSelectableFieldLabelConversions(t *testing.T) { + apitesting.TestSelectableFieldLabelConversionsOfKind(t, + testapi.Federation.GroupVersion().String(), + "Cluster", + labels.Set(ClusterToSelectableFields(&federation.Cluster{})), + nil, + ) +} diff --git a/hack/test-go.sh b/hack/test-go.sh index 422e2d8857d..55a57518ae1 100755 --- a/hack/test-go.sh +++ b/hack/test-go.sh @@ -58,7 +58,7 @@ KUBE_GOVERALLS_BIN=${KUBE_GOVERALLS_BIN:-} # Lists of API Versions of each groups that should be tested, groups are # separated by comma, lists are separated by semicolon. e.g., # "v1,compute/v1alpha1,experimental/v1alpha2;v1,compute/v2,experimental/v1alpha3" -KUBE_TEST_API_VERSIONS=${KUBE_TEST_API_VERSIONS:-"v1,extensions/v1beta1,metrics/v1alpha1;v1,autoscaling/v1,batch/v1,extensions/v1beta1,apps/v1alpha1,metrics/v1alpha1"} +KUBE_TEST_API_VERSIONS=${KUBE_TEST_API_VERSIONS:-"v1,extensions/v1beta1,metrics/v1alpha1,federation/v1alpha1;v1,autoscaling/v1,batch/v1,extensions/v1beta1,apps/v1alpha1,metrics/v1alpha1,federation/v1alpha1"} # once we have multiple group supports # Run tests with the standard (registry) and a custom etcd prefix # (kubernetes.io/registry). diff --git a/pkg/api/testapi/testapi.go b/pkg/api/testapi/testapi.go index 688cb33b98b..bbbe0367e98 100644 --- a/pkg/api/testapi/testapi.go +++ b/pkg/api/testapi/testapi.go @@ -23,6 +23,7 @@ import ( "reflect" "strings" + "k8s.io/kubernetes/federation/apis/federation" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" @@ -33,6 +34,7 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/runtime" + _ "k8s.io/kubernetes/federation/apis/federation/install" _ "k8s.io/kubernetes/pkg/api/install" _ "k8s.io/kubernetes/pkg/apis/apps/install" _ "k8s.io/kubernetes/pkg/apis/autoscaling/install" @@ -49,6 +51,7 @@ var ( Batch TestGroup Extensions TestGroup Apps TestGroup + Federation TestGroup ) type TestGroup struct { @@ -132,12 +135,20 @@ func init() { internalTypes: api.Scheme.KnownTypes(extensions.SchemeGroupVersion), } } + if _, ok := Groups[federation.GroupName]; !ok { + Groups[federation.GroupName] = TestGroup{ + externalGroupVersion: unversioned.GroupVersion{Group: federation.GroupName, Version: registered.GroupOrDie(federation.GroupName).GroupVersion.Version}, + internalGroupVersion: federation.SchemeGroupVersion, + internalTypes: api.Scheme.KnownTypes(federation.SchemeGroupVersion), + } + } Default = Groups[api.GroupName] Autoscaling = Groups[autoscaling.GroupName] Batch = Groups[batch.GroupName] Apps = Groups[apps.GroupName] Extensions = Groups[extensions.GroupName] + Federation = Groups[federation.GroupName] } func (g TestGroup) ContentConfig() (string, *unversioned.GroupVersion, runtime.Codec) {