From 2d13dfaf135cf5ef28f8fc64f1d166f3696d2d7b Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Thu, 12 Mar 2015 11:08:06 -0400 Subject: [PATCH] Improvements to namespace registry to align with pod model --- pkg/api/rest/types.go | 27 -- pkg/api/validation/validation.go | 12 + pkg/master/master.go | 10 +- pkg/master/publish.go | 2 +- pkg/registry/namespace/etcd/etcd.go | 101 +++++++ pkg/registry/namespace/etcd/etcd_test.go | 321 +++++++++++++++++++++++ pkg/registry/namespace/registry.go | 96 +++++-- pkg/registry/namespace/rest.go | 145 +++++----- pkg/registry/namespace/rest_test.go | 173 +----------- 9 files changed, 588 insertions(+), 299 deletions(-) create mode 100644 pkg/registry/namespace/etcd/etcd.go create mode 100644 pkg/registry/namespace/etcd/etcd_test.go diff --git a/pkg/api/rest/types.go b/pkg/api/rest/types.go index 3373b3ebb5b..6292bcaae4d 100644 --- a/pkg/api/rest/types.go +++ b/pkg/api/rest/types.go @@ -107,30 +107,3 @@ func (nodeStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { node := obj.(*api.Node) return validation.ValidateMinion(node) } - -// namespaceStrategy implements behavior for nodes -type namespaceStrategy struct { - runtime.ObjectTyper - api.NameGenerator -} - -// Namespaces is the default logic that applies when creating and updating Namespace -// objects. -var Namespaces RESTCreateStrategy = namespaceStrategy{api.Scheme, api.SimpleNameGenerator} - -// NamespaceScoped is false for namespaces. -func (namespaceStrategy) NamespaceScoped() bool { - return false -} - -// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. -func (namespaceStrategy) ResetBeforeCreate(obj runtime.Object) { - _ = obj.(*api.Namespace) - // Namespace allow *all* fields, including status, to be set. -} - -// Validate validates a new namespace. -func (namespaceStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { - namespace := obj.(*api.Namespace) - return validation.ValidateNamespace(namespace) -} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 170b2adcda1..4d69a18e109 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1025,3 +1025,15 @@ func ValidateNamespaceUpdate(oldNamespace *api.Namespace, namespace *api.Namespa } return allErrs } + +// ValidateNamespaceStatusUpdate tests to see if the update is legal for an end user to make. newNamespace is updated with fields +// that cannot be changed. +func ValidateNamespaceStatusUpdate(newNamespace, oldNamespace *api.Namespace) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldNamespace.ObjectMeta, &newNamespace.ObjectMeta).Prefix("metadata")...) + if newNamespace.Status.Phase != oldNamespace.Status.Phase { + allErrs = append(allErrs, errs.NewFieldInvalid("status.phase", newNamespace.Status.Phase, "namespace phase cannot be changed directly")) + } + newNamespace.Spec = oldNamespace.Spec + return allErrs +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 574ce24bde7..c2c8c0f3e0e 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -45,10 +45,10 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/event" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/limitrange" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" + namespaceetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" podetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod/etcd" resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd" @@ -159,7 +159,7 @@ type Master struct { // TODO: define the internal typed interface in a way that clients can // also be replaced nodeRegistry minion.Registry - namespaceRegistry generic.Registry + namespaceRegistry namespace.Registry serviceRegistry service.Registry endpointRegistry endpoint.Registry @@ -379,7 +379,9 @@ func (m *Master) init(c *Config) { resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewREST(c.EtcdHelper) secretRegistry := secret.NewEtcdRegistry(c.EtcdHelper) - m.namespaceRegistry = namespace.NewEtcdRegistry(c.EtcdHelper) + + namespaceStorage := namespaceetcd.NewREST(c.EtcdHelper) + m.namespaceRegistry = namespace.NewRegistry(namespaceStorage) // TODO: split me up into distinct storage registries registry := etcd.NewRegistry(c.EtcdHelper, podRegistry) @@ -421,7 +423,7 @@ func (m *Master) init(c *Config) { "limitRanges": limitrange.NewREST(limitRangeRegistry), "resourceQuotas": resourceQuotaStorage, "resourceQuotas/status": resourceQuotaStatusStorage, - "namespaces": namespace.NewREST(m.namespaceRegistry), + "namespaces": namespaceStorage, "secrets": secret.NewREST(secretRegistry), } diff --git a/pkg/master/publish.go b/pkg/master/publish.go index 3cfb43f73f4..2110f66b882 100644 --- a/pkg/master/publish.go +++ b/pkg/master/publish.go @@ -80,7 +80,7 @@ func (m *Master) roServiceWriterLoop(stop chan struct{}) { // createMasterNamespaceIfNeeded will create the namespace that contains the master services if it doesn't already exist func (m *Master) createMasterNamespaceIfNeeded(ns string) error { ctx := api.NewContext() - if _, err := m.namespaceRegistry.Get(ctx, api.NamespaceDefault); err == nil { + if _, err := m.namespaceRegistry.GetNamespace(ctx, api.NamespaceDefault); err == nil { // the namespace already exists return nil } diff --git a/pkg/registry/namespace/etcd/etcd.go b/pkg/registry/namespace/etcd/etcd.go new file mode 100644 index 00000000000..edc641f6f4e --- /dev/null +++ b/pkg/registry/namespace/etcd/etcd.go @@ -0,0 +1,101 @@ +/* +Copyright 2015 Google Inc. 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 ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// rest implements a RESTStorage for namespaces against etcd +type REST struct { + store *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against namespaces +func NewREST(h tools.EtcdHelper) *REST { + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.Namespace{} }, + NewListFunc: func() runtime.Object { return &api.NamespaceList{} }, + KeyRootFunc: func(ctx api.Context) string { + return "/registry/namespaces" + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return "/registry/namespaces/" + name, nil + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.Namespace).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return namespace.MatchNamespace(label, field) + }, + EndpointName: "namespaces", + Helper: h, + } + store.CreateStrategy = namespace.Strategy + store.UpdateStrategy = namespace.Strategy + store.ReturnDeletedObject = true + + return &REST{store: store} +} + +// New returns a new object +func (r *REST) New() runtime.Object { + return r.store.NewFunc() +} + +// NewList returns a new list object +func (r *REST) NewList() runtime.Object { + return r.store.NewListFunc() +} + +// List obtains a list of namespaces with labels that match selector. +func (r *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { + return r.store.List(ctx, label, field) +} + +// Watch begins watching for new, changed, or deleted namespaces +func (r *REST) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return r.store.Watch(ctx, label, field, resourceVersion) +} + +// Get gets a specific namespace specified by its name +func (r *REST) Get(ctx api.Context, name string) (runtime.Object, error) { + return r.store.Get(ctx, name) +} + +// Create creates a namespace based on a specification. +func (r *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { + return r.store.Create(ctx, obj) +} + +// Update changes a namespace specification. +func (r *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) +} + +// Delete deletes an existing namespace specified by its name +func (r *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { + return r.store.Delete(ctx, name) +} diff --git a/pkg/registry/namespace/etcd/etcd_test.go b/pkg/registry/namespace/etcd/etcd_test.go new file mode 100644 index 00000000000..1e71dead83b --- /dev/null +++ b/pkg/registry/namespace/etcd/etcd_test.go @@ -0,0 +1,321 @@ +/* +Copyright 2015 Google Inc. 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" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/coreos/go-etcd/etcd" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.EtcdHelper{Client: fakeEtcdClient, Codec: latest.Codec, ResourceVersioner: tools.RuntimeVersionAdapter{latest.ResourceVersioner}} + return fakeEtcdClient, helper +} + +func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient, h := newHelper(t) + storage := NewREST(h) + return storage, fakeEtcdClient, h +} + +func validNewNamespace() *api.Namespace { + return &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + } +} + +func validChangedNamespace() *api.Namespace { + namespace := validNewNamespace() + namespace.ResourceVersion = "1" + namespace.Labels = map[string]string{ + "foo": "bar", + } + return namespace +} + +func TestStorage(t *testing.T) { + storage, _, _ := newStorage(t) + namespace.NewRegistry(storage) +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + namespace := validNewNamespace() + namespace.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + namespace, + // invalid + &api.Namespace{ + ObjectMeta: api.ObjectMeta{Namespace: "nope"}, + }, + ) +} + +func expectNamespace(t *testing.T, out runtime.Object) (*api.Namespace, bool) { + namespace, ok := out.(*api.Namespace) + if !ok || namespace == nil { + t.Errorf("Expected an api.Namespace object, was %#v", out) + return nil, false + } + return namespace, true +} + +func TestCreateSetsFields(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage := NewREST(helper) + namespace := validNewNamespace() + _, err := storage.Create(api.NewDefaultContext(), namespace) + if err != fakeEtcdClient.Err { + t.Fatalf("unexpected error: %v", err) + } + + actual := &api.Namespace{} + if err := helper.ExtractObj("/registry/namespaces/foo", actual, false); err != nil { + t.Fatalf("unexpected extraction error: %v", err) + } + if actual.Name != namespace.Name { + t.Errorf("unexpected namespace: %#v", actual) + } + if len(actual.UID) == 0 { + t.Errorf("expected namespace UID to be set: %#v", actual) + } + if actual.Status.Phase != api.NamespaceActive { + t.Errorf("expected namespace phase to be set to active, but %v", actual.Status.Phase) + } +} + +func TestListEmptyNamespaceList(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.ChangeIndex = 1 + fakeEtcdClient.Data["/registry/namespaces"] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: fakeEtcdClient.NewError(tools.EtcdErrorCodeNotFound), + } + + storage := NewREST(helper) + namespaces, err := storage.List(api.NewContext(), labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if len(namespaces.(*api.NamespaceList).Items) != 0 { + t.Errorf("Unexpected non-zero namespace list: %#v", namespaces) + } + if namespaces.(*api.NamespaceList).ResourceVersion != "1" { + t.Errorf("Unexpected resource version: %#v", namespaces) + } +} + +func TestListNamespaceList(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/namespaces"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + } + storage := NewREST(helper) + namespacesObj, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything()) + namespaces := namespacesObj.(*api.NamespaceList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(namespaces.Items) != 2 { + t.Errorf("Unexpected namespaces list: %#v", namespaces) + } + if namespaces.Items[0].Name != "foo" || namespaces.Items[0].Status.Phase != api.NamespaceActive { + t.Errorf("Unexpected namespace: %#v", namespaces.Items[0]) + } + if namespaces.Items[1].Name != "bar" { + t.Errorf("Unexpected namespace: %#v", namespaces.Items[1]) + } +} + +func TestListNamespaceListSelection(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/namespaces"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + {Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "baz"}, + Status: api.NamespaceStatus{Phase: api.NamespaceTerminating}, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "qux", + Labels: map[string]string{"label": "qux"}, + }, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "zot"}, + })}, + }, + }, + }, + } + storage := NewREST(helper) + ctx := api.NewDefaultContext() + table := []struct { + label, field string + expectedIDs util.StringSet + }{ + { + expectedIDs: util.NewStringSet("foo", "bar", "baz", "qux", "zot"), + }, { + field: "name=zot", + expectedIDs: util.NewStringSet("zot"), + }, { + label: "label=qux", + expectedIDs: util.NewStringSet("qux"), + }, { + field: "status.phase=Terminating", + expectedIDs: util.NewStringSet("baz"), + }, + } + + for index, item := range table { + label, err := labels.Parse(item.label) + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + field, err := fields.ParseSelector(item.field) + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + namespacesObj, err := storage.List(ctx, label, field) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + namespaces := namespacesObj.(*api.NamespaceList) + + set := util.NewStringSet() + for i := range namespaces.Items { + set.Insert(namespaces.Items[i].Name) + } + if e, a := len(item.expectedIDs), len(set); e != a { + t.Errorf("%v: Expected %v, got %v", index, item.expectedIDs, set) + } + } +} + +func TestNamespaceDecode(t *testing.T) { + storage := NewREST(tools.EtcdHelper{}) + expected := validNewNamespace() + expected.Status.Phase = api.NamespaceActive + body, err := latest.Codec.Encode(expected) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual := storage.New() + if err := latest.Codec.DecodeInto(body, actual); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !api.Semantic.DeepEqual(expected, actual) { + t.Errorf("mismatch: %s", util.ObjectDiff(expected, actual)) + } +} + +func TestGet(t *testing.T) { + expect := validNewNamespace() + expect.Status.Phase = api.NamespaceActive + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/namespaces/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, expect), + }, + }, + } + storage := NewREST(helper) + obj, err := storage.Get(api.NewContext(), "foo") + namespace := obj.(*api.Namespace) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expect.Status.Phase = api.NamespaceActive + if e, a := expect, namespace; !api.Semantic.DeepEqual(e, a) { + t.Errorf("Unexpected namespace: %s", util.ObjectDiff(e, a)) + } +} + +func TestDeleteNamespace(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.ChangeIndex = 1 + fakeEtcdClient.Data["/registry/namespaces/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Status: api.NamespaceStatus{Phase: api.NamespaceActive}, + }), + ModifiedIndex: 1, + CreatedIndex: 1, + }, + }, + } + storage := NewREST(helper) + _, err := storage.Delete(api.NewDefaultContext(), "foo") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // TODO: when we add life-cycle, this will go to Terminating, and then we need to test Terminating to gone +} diff --git a/pkg/registry/namespace/registry.go b/pkg/registry/namespace/registry.go index 5c6186834f4..3add8a3bc27 100644 --- a/pkg/registry/namespace/registry.go +++ b/pkg/registry/namespace/registry.go @@ -18,31 +18,83 @@ package namespace import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" - etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -// registry implements custom changes to generic.Etcd for Namespace storage -type registry struct { - *etcdgeneric.Etcd +// Registry is an interface implemented by things that know how to store Namespace objects. +type Registry interface { + // ListNamespaces obtains a list of namespaces having labels which match selector. + ListNamespaces(ctx api.Context, selector labels.Selector) (*api.NamespaceList, error) + // Watch for new/changed/deleted namespaces + WatchNamespaces(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific namespace + GetNamespace(ctx api.Context, namespaceID string) (*api.Namespace, error) + // Create a namespace based on a specification. + CreateNamespace(ctx api.Context, namespace *api.Namespace) error + // Update an existing namespace + UpdateNamespace(ctx api.Context, namespace *api.Namespace) error + // Delete an existing namespace + DeleteNamespace(ctx api.Context, namespaceID string) error } -// NewEtcdRegistry returns a registry which will store Namespace objects in the given EtcdHelper. -func NewEtcdRegistry(h tools.EtcdHelper) generic.Registry { - return registry{ - Etcd: &etcdgeneric.Etcd{ - NewFunc: func() runtime.Object { return &api.Namespace{} }, - NewListFunc: func() runtime.Object { return &api.NamespaceList{} }, - EndpointName: "namespaces", - KeyRootFunc: func(ctx api.Context) string { - return "/registry/namespaces" - }, - KeyFunc: func(ctx api.Context, id string) (string, error) { - return "/registry/namespaces/" + id, nil - }, - Helper: h, - }, - } +// Storage is an interface for a standard REST Storage backend +// TODO: move me somewhere common +type Storage interface { + apiserver.RESTDeleter + apiserver.RESTLister + apiserver.RESTGetter + apiserver.ResourceWatcher + + Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) + Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) +} + +// storage puts strong typing around storage calls +type storage struct { + Storage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s Storage) Registry { + return &storage{s} +} + +func (s *storage) ListNamespaces(ctx api.Context, label labels.Selector) (*api.NamespaceList, error) { + obj, err := s.List(ctx, label, fields.Everything()) + if err != nil { + return nil, err + } + return obj.(*api.NamespaceList), nil +} + +func (s *storage) WatchNamespaces(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetNamespace(ctx api.Context, namespaceName string) (*api.Namespace, error) { + obj, err := s.Get(ctx, namespaceName) + if err != nil { + return nil, err + } + return obj.(*api.Namespace), nil +} + +func (s *storage) CreateNamespace(ctx api.Context, namespace *api.Namespace) error { + _, err := s.Create(ctx, namespace) + return err +} + +func (s *storage) UpdateNamespace(ctx api.Context, namespace *api.Namespace) error { + _, _, err := s.Update(ctx, namespace) + return err +} + +func (s *storage) DeleteNamespace(ctx api.Context, namespaceID string) error { + _, err := s.Delete(ctx, namespaceID) + return err } diff --git a/pkg/registry/namespace/rest.go b/pkg/registry/namespace/rest.go index 1810b7e0fac..cff82b4513d 100644 --- a/pkg/registry/namespace/rest.go +++ b/pkg/registry/namespace/rest.go @@ -20,108 +20,81 @@ import ( "fmt" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -// REST provides the RESTStorage access patterns to work with Namespace objects. -type REST struct { - registry generic.Registry +// namespaceStrategy implements behavior for Namespaces +type namespaceStrategy struct { + runtime.ObjectTyper + api.NameGenerator } -// NewREST returns a new REST. You must use a registry created by -// NewEtcdRegistry unless you're testing. -func NewREST(registry generic.Registry) *REST { - return &REST{ - registry: registry, - } +// Strategy is the default logic that applies when creating and updating Namespace +// objects via the REST API. +var Strategy = namespaceStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is true for namespaces. +func (namespaceStrategy) NamespaceScoped() bool { + return false } -// Create creates a Namespace object -func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (namespaceStrategy) ResetBeforeCreate(obj runtime.Object) { namespace := obj.(*api.Namespace) - if err := rest.BeforeCreate(rest.Namespaces, ctx, obj); err != nil { - return nil, err + namespace.Status = api.NamespaceStatus{ + Phase: api.NamespaceActive, } - if err := rs.registry.CreateWithName(ctx, namespace.Name, namespace); err != nil { - err = rest.CheckGeneratedNameError(rest.Namespaces, err, namespace) - return nil, err - } - return rs.registry.Get(ctx, namespace.Name) } -// Update updates a Namespace object. -func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { - namespace, ok := obj.(*api.Namespace) - if !ok { - return nil, false, fmt.Errorf("not a namespace: %#v", obj) - } - - oldObj, err := rs.registry.Get(ctx, namespace.Name) - if err != nil { - return nil, false, err - } - - oldNamespace := oldObj.(*api.Namespace) - if errs := validation.ValidateNamespaceUpdate(oldNamespace, namespace); len(errs) > 0 { - return nil, false, kerrors.NewInvalid("namespace", namespace.Name, errs) - } - - if err := rs.registry.UpdateWithName(ctx, oldNamespace.Name, oldNamespace); err != nil { - return nil, false, err - } - out, err := rs.registry.Get(ctx, oldNamespace.Name) - return out, false, err +// Validate validates a new namespace. +func (namespaceStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { + namespace := obj.(*api.Namespace) + return validation.ValidateNamespace(namespace) } -// Delete deletes the Namespace with the specified name -func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { - obj, err := rs.registry.Get(ctx, name) - if err != nil { - return nil, err +// AllowCreateOnUpdate is false for namespaces. +func (namespaceStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (namespaceStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + return validation.ValidateNamespaceUpdate(obj.(*api.Namespace), old.(*api.Namespace)) +} + +type namespaceStatusStrategy struct { + namespaceStrategy +} + +var StatusStrategy = namespaceStatusStrategy{Strategy} + +func (namespaceStatusStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + // TODO: merge valid fields after update + return validation.ValidateNamespaceStatusUpdate(obj.(*api.Namespace), old.(*api.Namespace)) +} + +// MatchNamespace returns a generic matcher for a given label and field selector. +func MatchNamespace(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + namespaceObj, ok := obj.(*api.Namespace) + if !ok { + return false, fmt.Errorf("not a namespace") + } + fields := NamespaceToSelectableFields(namespaceObj) + return label.Matches(labels.Set(namespaceObj.Labels)) && field.Matches(fields), nil + }) +} + +// NamespaceToSelectableFields returns a label set that represents the object +// TODO: fields are not labels, and the validation rules for them do not apply. +func NamespaceToSelectableFields(namespace *api.Namespace) labels.Set { + return labels.Set{ + "name": namespace.Name, + "status.phase": string(namespace.Status.Phase), } - _, ok := obj.(*api.Namespace) - if !ok { - return nil, fmt.Errorf("invalid object type") - } - return rs.registry.Delete(ctx, name) -} - -func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) { - obj, err := rs.registry.Get(ctx, id) - if err != nil { - return nil, err - } - namespace, ok := obj.(*api.Namespace) - if !ok { - return nil, fmt.Errorf("invalid object type") - } - return namespace, err -} - -func (rs *REST) getAttrs(obj runtime.Object) (objLabels labels.Set, objFields fields.Set, err error) { - return labels.Set{}, fields.Set{}, nil -} - -func (rs *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { - return rs.registry.ListPredicate(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}) -} - -func (rs *REST) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { - return rs.registry.WatchPredicate(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}, resourceVersion) -} - -// New returns a new api.Namespace -func (*REST) New() runtime.Object { - return &api.Namespace{} -} - -func (*REST) NewList() runtime.Object { - return &api.NamespaceList{} } diff --git a/pkg/registry/namespace/rest_test.go b/pkg/registry/namespace/rest_test.go index 9f15d992fbf..0b36fa87843 100644 --- a/pkg/registry/namespace/rest_test.go +++ b/pkg/registry/namespace/rest_test.go @@ -17,169 +17,24 @@ limitations under the License. package namespace import ( - "reflect" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -type testRegistry struct { - *registrytest.GenericRegistry -} - -func NewTestREST() (testRegistry, *REST) { - reg := testRegistry{registrytest.NewGeneric(nil)} - return reg, NewREST(reg) -} - -func testNamespace(name string) *api.Namespace { - return &api.Namespace{ - ObjectMeta: api.ObjectMeta{ - Name: name, - }, - } -} - -func TestRESTCreate(t *testing.T) { - table := []struct { - ctx api.Context - namespace *api.Namespace - valid bool - }{ - { - ctx: api.NewContext(), - namespace: testNamespace("foo"), - valid: true, - }, { - ctx: api.NewContext(), - namespace: testNamespace("bar"), - valid: true, - }, - } - - for _, item := range table { - _, rest := NewTestREST() - c, err := rest.Create(item.ctx, item.namespace) - if !item.valid { - if err == nil { - t.Errorf("unexpected non-error for %v", item.namespace.Name) - } - continue - } - if err != nil { - t.Errorf("%v: Unexpected error %v", item.namespace.Name, err) - continue - } - if !api.HasObjectMetaSystemFieldValues(&item.namespace.ObjectMeta) { - t.Errorf("storage did not populate object meta field values") - } - if e, a := item.namespace, c; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) - } - // Ensure we implement the interface - _ = apiserver.ResourceWatcher(rest) - } -} - -func TestRESTUpdate(t *testing.T) { - _, rest := NewTestREST() - namespaceA := testNamespace("foo") - _, err := rest.Create(api.NewDefaultContext(), namespaceA) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - got, err := rest.Get(api.NewDefaultContext(), namespaceA.Name) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if e, a := namespaceA, got; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) - } - namespaceB := testNamespace("foo") - _, _, err = rest.Update(api.NewDefaultContext(), namespaceB) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - got2, err := rest.Get(api.NewDefaultContext(), namespaceB.Name) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if e, a := namespaceB, got2; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) - } - -} - -func TestRESTDelete(t *testing.T) { - _, rest := NewTestREST() - namespaceA := testNamespace("foo") - _, err := rest.Create(api.NewContext(), namespaceA) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - c, err := rest.Delete(api.NewContext(), namespaceA.Name) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if stat := c.(*api.Status); stat.Status != api.StatusSuccess { - t.Errorf("unexpected status: %v", stat) - } -} - -func TestRESTGet(t *testing.T) { - _, rest := NewTestREST() - namespaceA := testNamespace("foo") - _, err := rest.Create(api.NewContext(), namespaceA) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - got, err := rest.Get(api.NewContext(), namespaceA.Name) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - if e, a := namespaceA, got; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) - } -} - -func TestRESTList(t *testing.T) { - reg, rest := NewTestREST() - namespaceA := testNamespace("foo") - namespaceB := testNamespace("bar") - namespaceC := testNamespace("baz") - reg.ObjectList = &api.NamespaceList{ - Items: []api.Namespace{*namespaceA, *namespaceB, *namespaceC}, - } - got, err := rest.List(api.NewContext(), labels.Everything(), fields.Everything()) - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - expect := &api.NamespaceList{ - Items: []api.Namespace{*namespaceA, *namespaceB, *namespaceC}, - } - if e, a := expect, got; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) - } -} - -func TestRESTWatch(t *testing.T) { - namespaceA := testNamespace("foo") - reg, rest := NewTestREST() - wi, err := rest.Watch(api.NewContext(), labels.Everything(), fields.Everything(), "0") - if err != nil { - t.Fatalf("Unexpected error %v", err) - } - go func() { - reg.Broadcaster.Action(watch.Added, namespaceA) - }() - got := <-wi.ResultChan() - if e, a := namespaceA, got.Object; !reflect.DeepEqual(e, a) { - t.Errorf("diff: %s", util.ObjectDiff(e, a)) +func TestNamespaceStrategy(t *testing.T) { + if Strategy.NamespaceScoped() { + t.Errorf("Namespaces should not be namespace scoped") + } + if Strategy.AllowCreateOnUpdate() { + t.Errorf("Namespaces should not allow create on update") + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Status: api.NamespaceStatus{Phase: api.NamespaceTerminating}, + } + Strategy.ResetBeforeCreate(namespace) + if namespace.Status.Phase != api.NamespaceActive { + t.Errorf("Namespaces do not allow setting phase on create") } }