diff --git a/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_clientset.go b/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_clientset.go index f06f8712915..e7606a72447 100644 --- a/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_clientset.go +++ b/cmd/libs/go2idl/client-gen/generators/fake/generator_fake_for_clientset.go @@ -111,9 +111,12 @@ func (g *genClientset) GenerateType(c *generator.Context, t *types.Type, w io.Wr // This part of code is version-independent, unchanging. var common = ` -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/fake/clientset_generated.go b/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/fake/clientset_generated.go index 52d83bb5c13..5468dc563bb 100644 --- a/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/fake/clientset_generated.go +++ b/cmd/libs/go2idl/client-gen/testoutput/clientset_generated/test_internalclientset/fake/clientset_generated.go @@ -29,9 +29,12 @@ import ( "k8s.io/kubernetes/pkg/watch" ) -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/federation/client/clientset_generated/federation_internalclientset/fake/clientset_generated.go b/federation/client/clientset_generated/federation_internalclientset/fake/clientset_generated.go index 21b95c03fb8..57f57192425 100644 --- a/federation/client/clientset_generated/federation_internalclientset/fake/clientset_generated.go +++ b/federation/client/clientset_generated/federation_internalclientset/fake/clientset_generated.go @@ -31,9 +31,12 @@ import ( "k8s.io/kubernetes/pkg/watch" ) -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/federation/client/clientset_generated/federation_release_1_3/fake/clientset_generated.go b/federation/client/clientset_generated/federation_release_1_3/fake/clientset_generated.go index edb4f79a880..5dff08c069a 100644 --- a/federation/client/clientset_generated/federation_release_1_3/fake/clientset_generated.go +++ b/federation/client/clientset_generated/federation_release_1_3/fake/clientset_generated.go @@ -31,9 +31,12 @@ import ( "k8s.io/kubernetes/pkg/watch" ) -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/pkg/client/clientset_generated/internalclientset/fake/clientset_generated.go b/pkg/client/clientset_generated/internalclientset/fake/clientset_generated.go index 36359d9fc17..811cab547b2 100644 --- a/pkg/client/clientset_generated/internalclientset/fake/clientset_generated.go +++ b/pkg/client/clientset_generated/internalclientset/fake/clientset_generated.go @@ -39,9 +39,12 @@ import ( "k8s.io/kubernetes/pkg/watch" ) -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/pkg/client/clientset_generated/release_1_3/fake/clientset_generated.go b/pkg/client/clientset_generated/release_1_3/fake/clientset_generated.go index 4c8913116f5..b109de502dd 100644 --- a/pkg/client/clientset_generated/release_1_3/fake/clientset_generated.go +++ b/pkg/client/clientset_generated/release_1_3/fake/clientset_generated.go @@ -35,9 +35,12 @@ import ( "k8s.io/kubernetes/pkg/watch" ) -// Clientset returns a clientset that will respond with the provided objects +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. func NewSimpleClientset(objects ...runtime.Object) *Clientset { - o := core.NewObjects(api.Scheme, api.Codecs.UniversalDecoder()) + o := core.NewObjectTracker(api.Scheme, api.Codecs.UniversalDecoder()) for _, obj := range objects { if err := o.Add(obj); err != nil { panic(err) diff --git a/pkg/client/testing/core/fake_test.go b/pkg/client/testing/core/fake_test.go new file mode 100644 index 00000000000..c872d5d2388 --- /dev/null +++ b/pkg/client/testing/core/fake_test.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. +*/ + +package core_test + +import ( + "testing" + + "k8s.io/kubernetes/pkg/api" + clientsetfake "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" +) + +func TestFakeClientSetFiltering(t *testing.T) { + tc := clientsetfake.NewSimpleClientset( + testPod("nsA", "pod-1"), + testPod("nsB", "pod-2"), + testSA("nsA", "sa-1"), + testSA("nsA", "sa-2"), + testSA("nsB", "sa-1"), + testSA("nsB", "sa-2"), + testSA("nsB", "sa-3"), + ) + + saList1, err := tc.Core().ServiceAccounts("nsA").List(api.ListOptions{}) + if err != nil { + t.Fatalf("ServiceAccounts.List: %s", err) + } + if actual, expected := len(saList1.Items), 2; expected != actual { + t.Fatalf("Expected %d records to match, got %d", expected, actual) + } + for _, sa := range saList1.Items { + if sa.Namespace != "nsA" { + t.Fatalf("Expected namespace %q; got %q", "nsA", sa.Namespace) + } + } + + saList2, err := tc.Core().ServiceAccounts("nsB").List(api.ListOptions{}) + if err != nil { + t.Fatalf("ServiceAccounts.List: %s", err) + } + if actual, expected := len(saList2.Items), 3; expected != actual { + t.Fatalf("Expected %d records to match, got %d", expected, actual) + } + for _, sa := range saList2.Items { + if sa.Namespace != "nsB" { + t.Fatalf("Expected namespace %q; got %q", "nsA", sa.Namespace) + } + } + + pod1, err := tc.Core().Pods("nsA").Get("pod-1") + if err != nil { + t.Fatalf("Pods.Get: %s", err) + } + if pod1 == nil { + t.Fatalf("Expected to find pod nsA/pod-1 but it wasn't found") + } + if pod1.Namespace != "nsA" || pod1.Name != "pod-1" { + t.Fatalf("Expected to find pod nsA/pod-1t, got %s/%s", pod1.Namespace, pod1.Name) + } + + wrongPod, err := tc.Core().Pods("nsB").Get("pod-1") + if err == nil { + t.Fatalf("Pods.Get: expected nsB/pod-1 not to match, but it matched %s/%s", wrongPod.Namespace, wrongPod.Name) + } + + allPods, err := tc.Core().Pods(api.NamespaceAll).List(api.ListOptions{}) + if err != nil { + t.Fatalf("Pods.List: %s", err) + } + if actual, expected := len(allPods.Items), 2; expected != actual { + t.Fatalf("Expected %d pods to match, got %d", expected, actual) + } + + allSAs, err := tc.Core().ServiceAccounts(api.NamespaceAll).List(api.ListOptions{}) + if err != nil { + t.Fatalf("ServiceAccounts.List: %s", err) + } + if actual, expected := len(allSAs.Items), 5; expected != actual { + t.Fatalf("Expected %d service accounts to match, got %d", expected, actual) + } +} + +func testSA(ns, name string) *api.ServiceAccount { + return &api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Namespace: ns, + Name: name, + }, + } +} + +func testPod(ns, name string) *api.Pod { + return &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Namespace: ns, + Name: name, + }, + } +} diff --git a/pkg/client/testing/core/fixture.go b/pkg/client/testing/core/fixture.go index bd4e560064e..96472bcbbe3 100644 --- a/pkg/client/testing/core/fixture.go +++ b/pkg/client/testing/core/fixture.go @@ -18,28 +18,39 @@ package core import ( "fmt" - "io/ioutil" - "reflect" - "strings" + "sync" "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/runtime" - "k8s.io/kubernetes/pkg/util/yaml" "k8s.io/kubernetes/pkg/watch" ) -// ObjectRetriever abstracts the implementation for retrieving or setting generic -// objects. It is intended to be used to fake calls to a server by returning -// objects based on their kind and name. -type ObjectRetriever interface { - // Kind should return a resource or a list of resources (depending on the provided kind and - // name). It should return an error if the caller should communicate an error to the server. - Kind(gvk unversioned.GroupVersionKind, name string) (runtime.Object, error) - // Add adds a runtime object for test purposes into this object. - Add(runtime.Object) error +// ObjectTracker keeps track of objects. It is intended to be used to +// fake calls to a server by returning objects based on their kind, +// namespace and name. +type ObjectTracker interface { + // Add adds an object to the tracker. If object being added + // is a list, its items are added separately. + Add(obj runtime.Object) error + + // Get retrieves the object by its kind, namespace and name. + Get(gvk unversioned.GroupVersionKind, ns, name string) (runtime.Object, error) + + // Update updates an existing object in the tracker. + Update(obj runtime.Object) error + + // List retrieves all objects of a given kind in the given + // namespace. Only non-List kinds are accepted. + List(gvk unversioned.GroupVersionKind, ns string) (runtime.Object, error) + + // Delete deletes an existing object from the tracker. If object + // didn't exist in the tracker prior to deletion, Delete returns + // no error. + Delete(gvk unversioned.GroupVersionKind, ns, name string) error } // ObjectScheme abstracts the implementation of common operations on objects. @@ -49,55 +60,78 @@ type ObjectScheme interface { runtime.ObjectTyper } -// ObjectReaction returns a ReactionFunc that takes a generic action string of the form -// - or -- and attempts to return a runtime -// Object or error that matches the requested action. For instance, list-replicationControllers -// should attempt to return a list of replication controllers. This method delegates to the -// ObjectRetriever interface to satisfy retrieval of lists or retrieval of single items. -// TODO: add support for sub resources -func ObjectReaction(o ObjectRetriever, mapper meta.RESTMapper) ReactionFunc { +// ObjectReaction returns a ReactionFunc that applies core.Action to +// the given tracker. +func ObjectReaction(tracker ObjectTracker, mapper meta.RESTMapper) ReactionFunc { return func(action Action) (bool, runtime.Object, error) { - resource := action.GetResource() - kind, err := mapper.KindFor(resource) + ns := action.GetNamespace() + gvr := action.GetResource() + + gvk, err := mapper.KindFor(gvr) + if err != nil { + return false, nil, fmt.Errorf("error getting kind for resource %q: %s", gvr, err) + } + // This is a temporary fix. Because there is no internal resource, so // the caller has no way to express that it expects to get an internal // kind back. A more proper fix will be directly specify the Kind when // build the action. - kind.Version = resource.Version - if err != nil { - return false, nil, fmt.Errorf("unrecognized action %s: %v", action.GetResource(), err) + gvk.Version = gvr.Version + if len(gvk.Version) == 0 { + gvk.Version = runtime.APIVersionInternal } - // TODO: have mapper return a Kind for a subresource? - switch castAction := action.(type) { - case ListAction: - kind.Kind += "List" - resource, err := o.Kind(kind, "") - return true, resource, err + // Here and below we need to switch on implementation types, + // not on interfaces, as some interfaces are identical + // (e.g. UpdateAction and CreateAction), so if we use them, + // updates and creates end up matching the same case branch. + switch action := action.(type) { - case GetAction: - resource, err := o.Kind(kind, castAction.GetName()) - return true, resource, err + case ListActionImpl: + obj, err := tracker.List(gvk, ns) + return true, obj, err - case DeleteAction: - resource, err := o.Kind(kind, castAction.GetName()) - return true, resource, err + case GetActionImpl: + obj, err := tracker.Get(gvk, ns, action.GetName()) + return true, obj, err - case CreateAction: - accessor, err := meta.Accessor(castAction.GetObject()) + case CreateActionImpl: + objMeta, err := meta.Accessor(action.GetObject()) if err != nil { return true, nil, err } - resource, err := o.Kind(kind, accessor.GetName()) - return true, resource, err - - case UpdateAction: - accessor, err := meta.Accessor(castAction.GetObject()) + if action.GetSubresource() == "" { + err = tracker.Add(action.GetObject()) + } else { + // TODO: Currently we're handling subresource creation as an update + // on the enclosing resource. This works for some subresources but + // might not be generic enough. + err = tracker.Update(action.GetObject()) + } if err != nil { return true, nil, err } - resource, err := o.Kind(kind, accessor.GetName()) - return true, resource, err + obj, err := tracker.Get(gvk, ns, objMeta.GetName()) + return true, obj, err + + case UpdateActionImpl: + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return true, nil, err + } + err = tracker.Update(action.GetObject()) + if err != nil { + return true, nil, err + } + obj, err := tracker.Get(gvk, ns, objMeta.GetName()) + return true, obj, err + + case DeleteActionImpl: + err := tracker.Delete(gvk, ns, action.GetName()) + if err != nil { + return true, nil, err + } + return true, nil, nil default: return false, nil, fmt.Errorf("no reaction implemented for %s", action) @@ -105,140 +139,276 @@ func ObjectReaction(o ObjectRetriever, mapper meta.RESTMapper) ReactionFunc { } } -// AddObjectsFromPath loads the JSON or YAML file containing Kubernetes API resources -// and adds them to the provided ObjectRetriever. -func AddObjectsFromPath(path string, o ObjectRetriever, decoder runtime.Decoder) error { - data, err := ioutil.ReadFile(path) +type tracker struct { + scheme ObjectScheme + decoder runtime.Decoder + lock sync.RWMutex + objects map[unversioned.GroupVersionKind][]runtime.Object +} + +var _ ObjectTracker = &tracker{} + +// NewObjectTracker returns an ObjectTracker that can be used to keep track +// of objects for the fake clientset. Mostly useful for unit tests. +func NewObjectTracker(scheme ObjectScheme, decoder runtime.Decoder) ObjectTracker { + return &tracker{ + scheme: scheme, + decoder: decoder, + objects: make(map[unversioned.GroupVersionKind][]runtime.Object), + } +} + +func (t *tracker) List(gvk unversioned.GroupVersionKind, ns string) (runtime.Object, error) { + // Heuristic for list kind: original kind + List suffix. Might + // not always be true but this tracker has a pretty limited + // understanding of the actual API model. + listGVK := gvk + listGVK.Kind = listGVK.Kind + "List" + + list, err := t.scheme.New(listGVK) + if err != nil { + return nil, err + } + + if !meta.IsListType(list) { + return nil, fmt.Errorf("%q is not a list type", listGVK.Kind) + } + + t.lock.RLock() + defer t.lock.RUnlock() + + objs, ok := t.objects[gvk] + if !ok { + return list, nil + } + + matchingObjs, err := filterByNamespaceAndName(objs, ns, "") + if err != nil { + return nil, err + } + if err := meta.SetList(list, matchingObjs); err != nil { + return nil, err + } + if list, err = t.scheme.Copy(list); err != nil { + return nil, err + } + return list, nil +} + +func (t *tracker) Get(gvk unversioned.GroupVersionKind, ns, name string) (runtime.Object, error) { + if err := checkNamespace(gvk, ns); err != nil { + return nil, err + } + + errNotFound := errors.NewNotFound(unversioned.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, name) + + t.lock.RLock() + defer t.lock.RUnlock() + + objs, ok := t.objects[gvk] + if !ok { + return nil, errNotFound + } + + matchingObjs, err := filterByNamespaceAndName(objs, ns, name) + if err != nil { + return nil, err + } + if len(matchingObjs) == 0 { + return nil, errNotFound + } + if len(matchingObjs) > 1 { + return nil, fmt.Errorf("more than one object matched gvk %s, ns: %q name: %q", gvk, ns, name) + } + + // Only one object should match in the tracker if it works + // correctly, as Add/Update methods enforce kind/namespace/name + // uniqueness. + obj, err := t.scheme.Copy(matchingObjs[0]) + if err != nil { + return nil, err + } + + if status, ok := obj.(*unversioned.Status); ok { + if status.Details != nil { + status.Details.Kind = gvk.Kind + } + if status.Status != unversioned.StatusSuccess { + return nil, &errors.StatusError{ErrStatus: *status} + } + } + + return obj, nil +} + +func (t *tracker) Add(obj runtime.Object) error { + return t.add(obj, false) +} + +func (t *tracker) Update(obj runtime.Object) error { + return t.add(obj, true) +} + +func (t *tracker) add(obj runtime.Object, replaceExisting bool) error { + if meta.IsListType(obj) { + return t.addList(obj, replaceExisting) + } + + gvks, _, err := t.scheme.ObjectKinds(obj) if err != nil { return err } - data, err = yaml.ToJSON(data) + if len(gvks) == 0 { + return fmt.Errorf("no registered kinds for %v", obj) + } + + t.lock.Lock() + defer t.lock.Unlock() + + for _, gvk := range gvks { + gr := unversioned.GroupResource{Group: gvk.Group, Resource: gvk.Kind} + + // To avoid the object from being accidentally modified by caller + // after it's been added to the tracker, we always store the deep + // copy. + obj, err = t.scheme.Copy(obj) + if err != nil { + return err + } + + if status, ok := obj.(*unversioned.Status); ok && status.Details != nil { + gvk.Kind = status.Details.Kind + } + + newMeta, err := meta.Accessor(obj) + if err != nil { + return err + } + + if err := checkNamespace(gvk, newMeta.GetNamespace()); err != nil { + return err + } + + for i, existingObj := range t.objects[gvk] { + oldMeta, err := meta.Accessor(existingObj) + if err != nil { + return err + } + if oldMeta.GetNamespace() == newMeta.GetNamespace() && oldMeta.GetName() == newMeta.GetName() { + if replaceExisting { + t.objects[gvk][i] = obj + return nil + } + return errors.NewAlreadyExists(gr, newMeta.GetName()) + } + } + + if replaceExisting { + // Tried to update but no matching object was found. + return errors.NewNotFound(gr, newMeta.GetName()) + } + + t.objects[gvk] = append(t.objects[gvk], obj) + } + + return nil +} + +func (t *tracker) addList(obj runtime.Object, replaceExisting bool) error { + list, err := meta.ExtractList(obj) if err != nil { return err } - obj, err := runtime.Decode(decoder, data) - if err != nil { - return err + errs := runtime.DecodeList(list, t.decoder) + if len(errs) > 0 { + return errs[0] } - if err := o.Add(obj); err != nil { - return err + for _, obj := range list { + err := t.add(obj, replaceExisting) + if err != nil { + return err + } } return nil } -type objects struct { - types map[string][]runtime.Object - last map[string]int - scheme ObjectScheme - decoder runtime.Decoder -} - -var _ ObjectRetriever = &objects{} - -// NewObjects implements the ObjectRetriever interface by introspecting the -// objects provided to Add() and returning them when the Kind method is invoked. -// If an api.List object is provided to Add(), each child item is added. If an -// object is added that is itself a list (PodList, ServiceList) then that is added -// to the "PodList" kind. If no PodList is added, the retriever will take any loaded -// Pods and return them in a list. If an api.Status is added, and the Details.Kind field -// is set, that status will be returned instead (as an error if Status != Success, or -// as a runtime.Object if Status == Success). If multiple PodLists are provided, they -// will be returned in order by the Kind call, and the last PodList will be reused for -// subsequent calls. -func NewObjects(scheme ObjectScheme, decoder runtime.Decoder) ObjectRetriever { - return objects{ - types: make(map[string][]runtime.Object), - last: make(map[string]int), - scheme: scheme, - decoder: decoder, - } -} - -func (o objects) Kind(kind unversioned.GroupVersionKind, name string) (runtime.Object, error) { - if len(kind.Version) == 0 { - kind.Version = runtime.APIVersionInternal - } - empty, err := o.scheme.New(kind) - nilValue := reflect.Zero(reflect.TypeOf(empty)).Interface().(runtime.Object) - - arr, ok := o.types[kind.Kind] - if !ok { - if strings.HasSuffix(kind.Kind, "List") { - itemKind := kind.Kind[:len(kind.Kind)-4] - arr, ok := o.types[itemKind] - if !ok { - return empty, nil - } - out, err := o.scheme.New(kind) - if err != nil { - return nilValue, err - } - if err := meta.SetList(out, arr); err != nil { - return nilValue, err - } - if out, err = o.scheme.Copy(out); err != nil { - return nilValue, err - } - return out, nil - } - return nilValue, errors.NewNotFound(unversioned.GroupResource{Group: kind.Group, Resource: kind.Kind}, name) - } - - index := o.last[kind.Kind] - if index >= len(arr) { - index = len(arr) - 1 - } - if index < 0 { - return nilValue, errors.NewNotFound(unversioned.GroupResource{Group: kind.Group, Resource: kind.Kind}, name) - } - out, err := o.scheme.Copy(arr[index]) - if err != nil { - return nilValue, err - } - o.last[kind.Kind] = index + 1 - - if status, ok := out.(*unversioned.Status); ok { - if status.Details != nil { - status.Details.Kind = kind.Kind - } - if status.Status != unversioned.StatusSuccess { - return nilValue, &errors.StatusError{ErrStatus: *status} - } - } - - return out, nil -} - -func (o objects) Add(obj runtime.Object) error { - gvks, _, err := o.scheme.ObjectKinds(obj) - if err != nil { +func (t *tracker) Delete(gvk unversioned.GroupVersionKind, ns, name string) error { + if err := checkNamespace(gvk, ns); err != nil { return err } - kind := gvks[0].Kind - switch { - case meta.IsListType(obj): - if kind != "List" { - o.types[kind] = append(o.types[kind], obj) - } + t.lock.Lock() + defer t.lock.Unlock() - list, err := meta.ExtractList(obj) + found := false + + for i, existingObj := range t.objects[gvk] { + objMeta, err := meta.Accessor(existingObj) if err != nil { return err } - if errs := runtime.DecodeList(list, o.decoder); len(errs) > 0 { - return errs[0] + if objMeta.GetNamespace() == ns && objMeta.GetName() == name { + t.objects[gvk] = append(t.objects[gvk][:i], t.objects[gvk][i+1:]...) + found = true } - for _, obj := range list { - if err := o.Add(obj); err != nil { - return err + } + + if found { + return nil + } + + return errors.NewNotFound(unversioned.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, name) +} + +// filterByNamespaceAndName returns all objects in the collection that +// match provided namespace and name. Empty namespace matches +// non-namespaced objects. +func filterByNamespaceAndName(objs []runtime.Object, ns, name string) ([]runtime.Object, error) { + var res []runtime.Object + + for _, obj := range objs { + acc, err := meta.Accessor(obj) + if err != nil { + return nil, err + } + if ns != "" && acc.GetNamespace() != ns { + continue + } + if name != "" && acc.GetName() != name { + continue + } + res = append(res, obj) + } + + return res, nil +} + +// checkNamespace makes sure that the scope of gvk matches ns. It +// returns an error if namespace is empty but gvk is a namespaced +// kind, or if ns is non-empty and gvk is a namespaced kind. +func checkNamespace(gvk unversioned.GroupVersionKind, ns string) error { + group, err := registered.Group(gvk.Group) + if err != nil { + return err + } + mapping, err := group.RESTMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return err + } + switch mapping.Scope.Name() { + case meta.RESTScopeNameRoot: + if ns != "" { + return fmt.Errorf("namespace specified for a non-namespaced kind %s", gvk) + } + case meta.RESTScopeNameNamespace: + if ns == "" { + // Skipping this check for Events, since + // controllers emit events that have no namespace, + // even though Event is a namespaced resource. + if gvk.Kind != "Event" { + return fmt.Errorf("no namespace specified for a namespaced kind %s", gvk) } } - default: - if status, ok := obj.(*unversioned.Status); ok && status.Details != nil { - kind = status.Details.Kind - } - o.types[kind] = append(o.types[kind], obj) } return nil diff --git a/pkg/controller/deployment/deployment_controller_test.go b/pkg/controller/deployment/deployment_controller_test.go index b2fcd521bf7..a192dcc22bb 100644 --- a/pkg/controller/deployment/deployment_controller_test.go +++ b/pkg/controller/deployment/deployment_controller_test.go @@ -44,6 +44,7 @@ func rs(name string, replicas int, selector map[string]string, timestamp unversi ObjectMeta: api.ObjectMeta{ Name: name, CreationTimestamp: timestamp, + Namespace: api.NamespaceDefault, }, Spec: exp.ReplicaSetSpec{ Replicas: int32(replicas), @@ -64,7 +65,8 @@ func newRSWithStatus(name string, specReplicas, statusReplicas int, selector map func deployment(name string, replicas int, maxSurge, maxUnavailable intstr.IntOrString, selector map[string]string) exp.Deployment { return exp.Deployment{ ObjectMeta: api.ObjectMeta{ - Name: name, + Name: name, + Namespace: api.NamespaceDefault, }, Spec: exp.DeploymentSpec{ Replicas: int32(replicas), @@ -142,10 +144,6 @@ func newReplicaSet(d *exp.Deployment, name string, replicas int) *exp.ReplicaSet } } -func newListOptions() api.ListOptions { - return api.ListOptions{} -} - // TestScale tests proportional scaling of deployments. Note that fenceposts for // rolling out (maxUnavailable, maxSurge) have no meaning for simple scaling other // than recording maxSurge as part of the max-replicas annotation that is taken @@ -966,22 +964,25 @@ type fixture struct { // Actions expected to happen on the client. Objects from here are also // preloaded into NewSimpleFake. actions []core.Action - objects *api.List + objects []runtime.Object } func (f *fixture) expectUpdateDeploymentAction(d *exp.Deployment) { f.actions = append(f.actions, core.NewUpdateAction(unversioned.GroupVersionResource{Resource: "deployments"}, d.Namespace, d)) - f.objects.Items = append(f.objects.Items, d) +} + +func (f *fixture) expectUpdateDeploymentStatusAction(d *exp.Deployment) { + action := core.NewUpdateAction(unversioned.GroupVersionResource{Resource: "deployments"}, d.Namespace, d) + action.Subresource = "status" + f.actions = append(f.actions, action) } func (f *fixture) expectCreateRSAction(rs *exp.ReplicaSet) { f.actions = append(f.actions, core.NewCreateAction(unversioned.GroupVersionResource{Resource: "replicasets"}, rs.Namespace, rs)) - f.objects.Items = append(f.objects.Items, rs) } func (f *fixture) expectUpdateRSAction(rs *exp.ReplicaSet) { f.actions = append(f.actions, core.NewUpdateAction(unversioned.GroupVersionResource{Resource: "replicasets"}, rs.Namespace, rs)) - f.objects.Items = append(f.objects.Items, rs) } func (f *fixture) expectListPodAction(namespace string, opt api.ListOptions) { @@ -991,12 +992,12 @@ func (f *fixture) expectListPodAction(namespace string, opt api.ListOptions) { func newFixture(t *testing.T) *fixture { f := &fixture{} f.t = t - f.objects = &api.List{} + f.objects = []runtime.Object{} return f } func (f *fixture) run(deploymentName string) { - f.client = fake.NewSimpleClientset(f.objects) + f.client = fake.NewSimpleClientset(f.objects...) c := NewDeploymentController(f.client, controller.NoResyncPeriodFunc) c.eventRecorder = &record.FakeRecorder{} c.rsStoreSynced = alwaysReady @@ -1040,16 +1041,13 @@ func TestSyncDeploymentCreatesReplicaSet(t *testing.T) { d := newDeployment(1, nil) f.dStore = append(f.dStore, d) + f.objects = append(f.objects, d) - // expect that one ReplicaSet with zero replicas is created - // then is updated to 1 replica - rs := newReplicaSet(d, "deploymentrs-4186632231", 0) - updatedRS := newReplicaSet(d, "deploymentrs-4186632231", 1) + rs := newReplicaSet(d, "deploymentrs-4186632231", 1) f.expectCreateRSAction(rs) f.expectUpdateDeploymentAction(d) - f.expectUpdateRSAction(updatedRS) - f.expectUpdateDeploymentAction(d) + f.expectUpdateDeploymentStatusAction(d) f.run(getKey(d, t)) } diff --git a/pkg/controller/namespace/namespace_controller_test.go b/pkg/controller/namespace/namespace_controller_test.go index f03a59d7130..9c5b77d8efe 100644 --- a/pkg/controller/namespace/namespace_controller_test.go +++ b/pkg/controller/namespace/namespace_controller_test.go @@ -133,8 +133,9 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, versions *unversioned.APIV testNamespace: testNamespacePendingFinalize, kubeClientActionSet: sets.NewString( strings.Join([]string{"get", "namespaces", ""}, "-"), - strings.Join([]string{"list", "pods", ""}, "-"), strings.Join([]string{"create", "namespaces", "finalize"}, "-"), + strings.Join([]string{"list", "pods", ""}, "-"), + strings.Join([]string{"delete", "namespaces", ""}, "-"), ), dynamicClientActionSet: dynamicClientActionSet, }, diff --git a/pkg/controller/node/nodecontroller.go b/pkg/controller/node/nodecontroller.go index ff7ebb4b7fd..578181d26ca 100644 --- a/pkg/controller/node/nodecontroller.go +++ b/pkg/controller/node/nodecontroller.go @@ -1005,9 +1005,9 @@ func (nc *NodeController) markAllPodsNotReady(nodeName string) error { if cond.Type == api.PodReady { pod.Status.Conditions[i].Status = api.ConditionFalse glog.V(2).Infof("Updating ready status of pod %v to false", pod.Name) - pod, err := nc.kubeClient.Core().Pods(pod.Namespace).UpdateStatus(&pod) + _, err := nc.kubeClient.Core().Pods(pod.Namespace).UpdateStatus(&pod) if err != nil { - glog.Warningf("Failed to update status for pod %q: %v", format.Pod(pod), err) + glog.Warningf("Failed to update status for pod %q: %v", format.Pod(&pod), err) errMsg = append(errMsg, fmt.Sprintf("%v", err)) } break diff --git a/pkg/controller/node/nodecontroller_test.go b/pkg/controller/node/nodecontroller_test.go index 967a7e946d3..818be93f22d 100644 --- a/pkg/controller/node/nodecontroller_test.go +++ b/pkg/controller/node/nodecontroller_test.go @@ -1426,8 +1426,25 @@ func newNode(name string) *api.Node { } func newPod(name, host string) *api.Pod { - return &api.Pod{ObjectMeta: api.ObjectMeta{Name: name}, Spec: api.PodSpec{NodeName: host}, - Status: api.PodStatus{Conditions: []api.PodCondition{{Type: api.PodReady, Status: api.ConditionTrue}}}} + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Namespace: "default", + Name: name, + }, + Spec: api.PodSpec{ + NodeName: host, + }, + Status: api.PodStatus{ + Conditions: []api.PodCondition{ + { + Type: api.PodReady, + Status: api.ConditionTrue, + }, + }, + }, + } + + return pod } func contains(node *api.Node, nodes []*api.Node) bool { diff --git a/pkg/controller/resourcequota/resource_quota_controller_test.go b/pkg/controller/resourcequota/resource_quota_controller_test.go index 71ef5df79b4..6fb82b9a5e4 100644 --- a/pkg/controller/resourcequota/resource_quota_controller_test.go +++ b/pkg/controller/resourcequota/resource_quota_controller_test.go @@ -162,6 +162,10 @@ func TestSyncResourceQuota(t *testing.T) { func TestSyncResourceQuotaSpecChange(t *testing.T) { resourceQuota := api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Namespace: "default", + Name: "rq", + }, Spec: api.ResourceQuotaSpec{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("4"), @@ -250,6 +254,10 @@ func TestSyncResourceQuotaSpecChange(t *testing.T) { func TestSyncResourceQuotaNoChange(t *testing.T) { resourceQuota := api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Namespace: "default", + Name: "rq", + }, Spec: api.ResourceQuotaSpec{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("4"), diff --git a/pkg/controller/serviceaccount/tokens_controller_test.go b/pkg/controller/serviceaccount/tokens_controller_test.go index 4fad2775279..3442b896dc9 100644 --- a/pkg/controller/serviceaccount/tokens_controller_test.go +++ b/pkg/controller/serviceaccount/tokens_controller_test.go @@ -223,7 +223,7 @@ func TestTokenCreation(t *testing.T) { ExpectedActions []core.Action }{ "new serviceaccount with no secrets": { - ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ @@ -233,7 +233,7 @@ func TestTokenCreation(t *testing.T) { }, }, "new serviceaccount with no secrets encountering create error": { - ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, MaxRetries: 10, IsAsync: true, Reactors: []reaction{{ @@ -250,7 +250,6 @@ func TestTokenCreation(t *testing.T) { } }, }}, - AddedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ // Attempt 1 @@ -295,7 +294,7 @@ func TestTokenCreation(t *testing.T) { }, }, "new serviceaccount with missing secrets": { - ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())}, AddedServiceAccount: serviceAccount(missingSecretReferences()), ExpectedActions: []core.Action{ @@ -305,7 +304,7 @@ func TestTokenCreation(t *testing.T) { }, }, "new serviceaccount with non-token secrets": { - ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()}, + ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()}, AddedServiceAccount: serviceAccount(regularSecretReferences()), ExpectedActions: []core.Action{ @@ -329,9 +328,8 @@ func TestTokenCreation(t *testing.T) { core.NewGetAction(unversioned.GroupVersionResource{Resource: "serviceaccounts"}, api.NamespaceDefault, "default"), }, }, - "updated serviceaccount with no secrets": { - ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())}, UpdatedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ @@ -341,7 +339,7 @@ func TestTokenCreation(t *testing.T) { }, }, "updated serviceaccount with missing secrets": { - ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())}, UpdatedServiceAccount: serviceAccount(missingSecretReferences()), ExpectedActions: []core.Action{ @@ -351,7 +349,7 @@ func TestTokenCreation(t *testing.T) { }, }, "updated serviceaccount with non-token secrets": { - ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()}, + ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()}, UpdatedServiceAccount: serviceAccount(regularSecretReferences()), ExpectedActions: []core.Action{ @@ -367,7 +365,7 @@ func TestTokenCreation(t *testing.T) { ExpectedActions: []core.Action{}, }, "updated serviceaccount with no secrets with resource conflict": { - ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences()), createdTokenSecret()}, + ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences())}, UpdatedServiceAccount: serviceAccount(emptySecretReferences()), ExpectedActions: []core.Action{ diff --git a/pkg/kubectl/describe_test.go b/pkg/kubectl/describe_test.go index 38119186b39..54f7f60219b 100644 --- a/pkg/kubectl/describe_test.go +++ b/pkg/kubectl/describe_test.go @@ -578,6 +578,9 @@ func TestDescribeEvents(t *testing.T) { events := &api.EventList{ Items: []api.Event{ { + ObjectMeta: api.ObjectMeta{ + Namespace: "foo", + }, Source: api.EventSource{Component: "kubelet"}, Message: "Item 1", FirstTimestamp: unversioned.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), diff --git a/pkg/kubelet/status/status_manager_test.go b/pkg/kubelet/status/status_manager_test.go index e88de8bcfe7..e6130540c73 100644 --- a/pkg/kubelet/status/status_manager_test.go +++ b/pkg/kubelet/status/status_manager_test.go @@ -526,16 +526,12 @@ func TestStaticPodStatus(t *testing.T) { mirrorPod.UID = "new-mirror-pod" mirrorPod.Status = api.PodStatus{} m.podManager.AddPod(mirrorPod) - // Expect update to new mirrorPod. + + // Expect no update to mirror pod, since UID has changed. m.testSyncBatch() verifyActions(t, m.kubeClient, []core.Action{ core.GetActionImpl{ActionImpl: core.ActionImpl{Verb: "get", Resource: unversioned.GroupVersionResource{Resource: "pods"}}}, - core.UpdateActionImpl{ActionImpl: core.ActionImpl{Verb: "update", Resource: unversioned.GroupVersionResource{Resource: "pods"}, Subresource: "status"}}, }) - updateAction = client.Actions()[1].(core.UpdateActionImpl) - updatedPod = updateAction.Object.(*api.Pod) - assert.Equal(t, mirrorPod.UID, updatedPod.UID, "Expected mirrorPod (%q), but got %q", mirrorPod.UID, updatedPod.UID) - assert.True(t, isStatusEqual(&status, &updatedPod.Status), "Expected: %+v, Got: %+v", status, updatedPod.Status) } func TestSetContainerReadiness(t *testing.T) { diff --git a/pkg/volume/configmap/configmap_test.go b/pkg/volume/configmap/configmap_test.go index cf0a54f402a..5078fc3eb95 100644 --- a/pkg/volume/configmap/configmap_test.go +++ b/pkg/volume/configmap/configmap_test.go @@ -228,7 +228,7 @@ func TestPlugin(t *testing.T) { t.Errorf("Can't find the plugin by name") } - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + pod := &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) if err != nil { t.Errorf("Failed to make a new Mounter: %v", err) @@ -283,7 +283,7 @@ func TestPluginReboot(t *testing.T) { t.Errorf("Can't find the plugin by name") } - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + pod := &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) if err != nil { t.Errorf("Failed to make a new Mounter: %v", err) diff --git a/pkg/volume/glusterfs/glusterfs_test.go b/pkg/volume/glusterfs/glusterfs_test.go index 524ec59da93..76939ba3657 100644 --- a/pkg/volume/glusterfs/glusterfs_test.go +++ b/pkg/volume/glusterfs/glusterfs_test.go @@ -212,7 +212,8 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { ep := &api.Endpoints{ ObjectMeta: api.ObjectMeta{ - Name: "ep", + Namespace: "nsA", + Name: "ep", }, Subsets: []api.EndpointSubset{{ Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, @@ -228,7 +229,7 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { // readOnly bool is supplied by persistent-claim volume source when its mounter creates other volumes spec := volume.NewSpecFromPersistentVolume(pv, true) - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} + pod := &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: "nsA", UID: types.UID("poduid")}} mounter, _ := plug.NewMounter(spec, pod, volume.VolumeOptions{}) if !mounter.GetAttributes().ReadOnly { diff --git a/pkg/volume/secret/secret_test.go b/pkg/volume/secret/secret_test.go index fff7e0bef2d..645d1e08e51 100644 --- a/pkg/volume/secret/secret_test.go +++ b/pkg/volume/secret/secret_test.go @@ -231,7 +231,7 @@ func TestPlugin(t *testing.T) { t.Errorf("Can't find the plugin by name") } - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + pod := &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) if err != nil { t.Errorf("Failed to make a new Mounter: %v", err) @@ -304,7 +304,7 @@ func TestPluginReboot(t *testing.T) { t.Errorf("Can't find the plugin by name") } - pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID}} + pod := &api.Pod{ObjectMeta: api.ObjectMeta{Namespace: testNamespace, UID: testPodUID}} mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) if err != nil { t.Errorf("Failed to make a new Mounter: %v", err) diff --git a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go index 186bc9f6eb8..b875997efb4 100644 --- a/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go +++ b/plugin/pkg/admission/security/podsecuritypolicy/admission_test.go @@ -1193,7 +1193,8 @@ func createNamespaceForTest() *kapi.Namespace { func createSAForTest() *kapi.ServiceAccount { return &kapi.ServiceAccount{ ObjectMeta: kapi.ObjectMeta{ - Name: "default", + Namespace: "default", + Name: "default", }, } }