mirror of
https://github.com/kubernetes/client-go.git
synced 2025-06-29 16:36:54 +00:00
Add fake client, informer factory, and lister to metadata client
These will be used by the garbage collector controller and others that use higher level primitives. Kubernetes-commit: bc89c37f32aa6cfd0f9ca975d9221d0a89320623
This commit is contained in:
parent
b83dc9a7d9
commit
cade5c0473
390
metadata/fake/simple.go
Normal file
390
metadata/fake/simple.go
Normal file
@ -0,0 +1,390 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 fake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/metadata"
|
||||
"k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
// MetadataClient assists in creating fake objects for use when testing, since metadata.Getter
|
||||
// does not expose create
|
||||
type MetadataClient interface {
|
||||
metadata.Getter
|
||||
CreateFake(obj *metav1.PartialObjectMetadata, opts metav1.CreateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error)
|
||||
UpdateFake(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error)
|
||||
}
|
||||
|
||||
// NewSimpleMetadataClient creates a new client that will use the provided scheme and respond with the
|
||||
// provided objects when requests are made. It will track actions made to the client which can be checked
|
||||
// with GetActions().
|
||||
func NewSimpleMetadataClient(scheme *runtime.Scheme, objects ...runtime.Object) *FakeMetadataClient {
|
||||
gvkFakeList := schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "List"}
|
||||
if !scheme.Recognizes(gvkFakeList) {
|
||||
// In order to use List with this client, you have to have the v1.List registered in your scheme, since this is a test
|
||||
// type we modify the input scheme
|
||||
scheme.AddKnownTypeWithName(gvkFakeList, &metav1.List{})
|
||||
}
|
||||
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
o := testing.NewObjectTracker(scheme, codecs.UniversalDeserializer())
|
||||
for _, obj := range objects {
|
||||
if err := o.Add(obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
cs := &FakeMetadataClient{scheme: scheme}
|
||||
cs.AddReactor("*", "*", testing.ObjectReaction(o))
|
||||
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
|
||||
gvr := action.GetResource()
|
||||
ns := action.GetNamespace()
|
||||
watch, err := o.Watch(gvr, ns)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return true, watch, nil
|
||||
})
|
||||
|
||||
return cs
|
||||
}
|
||||
|
||||
// FakeMetadataClient implements clientset.Interface. Meant to be embedded into a
|
||||
// struct to get a default implementation. This makes faking out just the method
|
||||
// you want to test easier.
|
||||
type FakeMetadataClient struct {
|
||||
testing.Fake
|
||||
scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
type metadataResourceClient struct {
|
||||
client *FakeMetadataClient
|
||||
namespace string
|
||||
resource schema.GroupVersionResource
|
||||
}
|
||||
|
||||
var _ metadata.Interface = &FakeMetadataClient{}
|
||||
|
||||
// Resource returns an interface for accessing the provided resource.
|
||||
func (c *FakeMetadataClient) Resource(resource schema.GroupVersionResource) metadata.Getter {
|
||||
return &metadataResourceClient{client: c, resource: resource}
|
||||
}
|
||||
|
||||
// Namespace returns an interface for accessing the current resource in the specified
|
||||
// namespace.
|
||||
func (c *metadataResourceClient) Namespace(ns string) metadata.ResourceInterface {
|
||||
ret := *c
|
||||
ret.namespace = ns
|
||||
return &ret
|
||||
}
|
||||
|
||||
// CreateFake records the object creation and processes it via the reactor.
|
||||
func (c *metadataResourceClient) CreateFake(obj *metav1.PartialObjectMetadata, opts metav1.CreateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
|
||||
var uncastRet runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootCreateAction(c.resource, obj), obj)
|
||||
|
||||
case len(c.namespace) == 0 && len(subresources) > 0:
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := accessor.GetName()
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), obj), obj)
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewCreateAction(c.resource, c.namespace, obj), obj)
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) > 0:
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := accessor.GetName()
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewCreateSubresourceAction(c.resource, name, strings.Join(subresources, "/"), c.namespace, obj), obj)
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uncastRet == nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, ok := uncastRet.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected return value type %T", uncastRet)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// UpdateFake records the object update and processes it via the reactor.
|
||||
func (c *metadataResourceClient) UpdateFake(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
|
||||
var uncastRet runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootUpdateAction(c.resource, obj), obj)
|
||||
|
||||
case len(c.namespace) == 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), obj), obj)
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewUpdateAction(c.resource, c.namespace, obj), obj)
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewUpdateSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, obj), obj)
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uncastRet == nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, ok := uncastRet.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected return value type %T", uncastRet)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// UpdateStatus records the object status update and processes it via the reactor.
|
||||
func (c *metadataResourceClient) UpdateStatus(obj *metav1.PartialObjectMetadata, opts metav1.UpdateOptions) (*metav1.PartialObjectMetadata, error) {
|
||||
var uncastRet runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootUpdateSubresourceAction(c.resource, "status", obj), obj)
|
||||
|
||||
case len(c.namespace) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewUpdateSubresourceAction(c.resource, "status", c.namespace, obj), obj)
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uncastRet == nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, ok := uncastRet.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected return value type %T", uncastRet)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Delete records the object deletion and processes it via the reactor.
|
||||
func (c *metadataResourceClient) Delete(name string, opts *metav1.DeleteOptions, subresources ...string) error {
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0 && len(subresources) == 0:
|
||||
_, err = c.client.Fake.
|
||||
Invokes(testing.NewRootDeleteAction(c.resource, name), &metav1.Status{Status: "metadata delete fail"})
|
||||
|
||||
case len(c.namespace) == 0 && len(subresources) > 0:
|
||||
_, err = c.client.Fake.
|
||||
Invokes(testing.NewRootDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata delete fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) == 0:
|
||||
_, err = c.client.Fake.
|
||||
Invokes(testing.NewDeleteAction(c.resource, c.namespace, name), &metav1.Status{Status: "metadata delete fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) > 0:
|
||||
_, err = c.client.Fake.
|
||||
Invokes(testing.NewDeleteSubresourceAction(c.resource, strings.Join(subresources, "/"), c.namespace, name), &metav1.Status{Status: "metadata delete fail"})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteCollection records the object collection deletion and processes it via the reactor.
|
||||
func (c *metadataResourceClient) DeleteCollection(opts *metav1.DeleteOptions, listOptions metav1.ListOptions) error {
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0:
|
||||
action := testing.NewRootDeleteCollectionAction(c.resource, listOptions)
|
||||
_, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "metadata deletecollection fail"})
|
||||
|
||||
case len(c.namespace) > 0:
|
||||
action := testing.NewDeleteCollectionAction(c.resource, c.namespace, listOptions)
|
||||
_, err = c.client.Fake.Invokes(action, &metav1.Status{Status: "metadata deletecollection fail"})
|
||||
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Get records the object retrieval and processes it via the reactor.
|
||||
func (c *metadataResourceClient) Get(name string, opts metav1.GetOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
|
||||
var uncastRet runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootGetAction(c.resource, name), &metav1.Status{Status: "metadata get fail"})
|
||||
|
||||
case len(c.namespace) == 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootGetSubresourceAction(c.resource, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata get fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewGetAction(c.resource, c.namespace, name), &metav1.Status{Status: "metadata get fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewGetSubresourceAction(c.resource, c.namespace, strings.Join(subresources, "/"), name), &metav1.Status{Status: "metadata get fail"})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uncastRet == nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, ok := uncastRet.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected return value type %T", uncastRet)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// List records the object deletion and processes it via the reactor.
|
||||
func (c *metadataResourceClient) List(opts metav1.ListOptions) (*metav1.PartialObjectMetadataList, error) {
|
||||
var obj runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0:
|
||||
obj, err = c.client.Fake.
|
||||
Invokes(testing.NewRootListAction(c.resource, schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, opts), &metav1.Status{Status: "metadata list fail"})
|
||||
|
||||
case len(c.namespace) > 0:
|
||||
obj, err = c.client.Fake.
|
||||
Invokes(testing.NewListAction(c.resource, schema.GroupVersionKind{Group: "fake-metadata-client-group", Version: "v1", Kind: "" /*List is appended by the tracker automatically*/}, c.namespace, opts), &metav1.Status{Status: "metadata list fail"})
|
||||
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
label, _, _ := testing.ExtractFromListOptions(opts)
|
||||
if label == nil {
|
||||
label = labels.Everything()
|
||||
}
|
||||
|
||||
inputList, ok := obj.(*metav1.List)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("incoming object is incorrect type %T", obj)
|
||||
}
|
||||
fmt.Printf("DEBUG: %#v\n", inputList)
|
||||
|
||||
list := &metav1.PartialObjectMetadataList{
|
||||
ListMeta: inputList.ListMeta,
|
||||
}
|
||||
for i := range inputList.Items {
|
||||
item, ok := inputList.Items[i].Object.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("item %d in list %T is %T", i, inputList, inputList.Items[i].Object)
|
||||
}
|
||||
metadata, err := meta.Accessor(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if label.Matches(labels.Set(metadata.GetLabels())) {
|
||||
list.Items = append(list.Items, *item)
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (c *metadataResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
|
||||
switch {
|
||||
case len(c.namespace) == 0:
|
||||
return c.client.Fake.
|
||||
InvokesWatch(testing.NewRootWatchAction(c.resource, opts))
|
||||
|
||||
case len(c.namespace) > 0:
|
||||
return c.client.Fake.
|
||||
InvokesWatch(testing.NewWatchAction(c.resource, c.namespace, opts))
|
||||
|
||||
}
|
||||
|
||||
panic("math broke")
|
||||
}
|
||||
|
||||
// Patch records the object patch and processes it via the reactor.
|
||||
func (c *metadataResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*metav1.PartialObjectMetadata, error) {
|
||||
var uncastRet runtime.Object
|
||||
var err error
|
||||
switch {
|
||||
case len(c.namespace) == 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootPatchAction(c.resource, name, pt, data), &metav1.Status{Status: "metadata patch fail"})
|
||||
|
||||
case len(c.namespace) == 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewRootPatchSubresourceAction(c.resource, name, pt, data, subresources...), &metav1.Status{Status: "metadata patch fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) == 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewPatchAction(c.resource, c.namespace, name, pt, data), &metav1.Status{Status: "metadata patch fail"})
|
||||
|
||||
case len(c.namespace) > 0 && len(subresources) > 0:
|
||||
uncastRet, err = c.client.Fake.
|
||||
Invokes(testing.NewPatchSubresourceAction(c.resource, c.namespace, name, pt, data, subresources...), &metav1.Status{Status: "metadata patch fail"})
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uncastRet == nil {
|
||||
return nil, err
|
||||
}
|
||||
ret, ok := uncastRet.(*metav1.PartialObjectMetadata)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected return value type %T", uncastRet)
|
||||
}
|
||||
return ret, err
|
||||
}
|
201
metadata/fake/simple_test.go
Normal file
201
metadata/fake/simple_test.go
Normal file
@ -0,0 +1,201 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 fake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
)
|
||||
|
||||
const (
|
||||
testGroup = "testgroup"
|
||||
testVersion = "testversion"
|
||||
testResource = "testkinds"
|
||||
testNamespace = "testns"
|
||||
testName = "testname"
|
||||
testKind = "TestKind"
|
||||
testAPIVersion = "testgroup/testversion"
|
||||
)
|
||||
|
||||
var scheme *runtime.Scheme
|
||||
|
||||
func init() {
|
||||
scheme = runtime.NewScheme()
|
||||
metav1.AddMetaToScheme(scheme)
|
||||
}
|
||||
|
||||
func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata {
|
||||
return &metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: apiVersion,
|
||||
Kind: kind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newPartialObjectMetadataWithAnnotations(annotations map[string]string) *metav1.PartialObjectMetadata {
|
||||
u := newPartialObjectMetadata(testAPIVersion, testKind, testNamespace, testName)
|
||||
u.Annotations = annotations
|
||||
return u
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
client := NewSimpleMetadataClient(scheme,
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group2/version", "TheKind", "ns-foo", "name2-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"),
|
||||
newPartialObjectMetadata("group2/version", "TheKind", "ns-foo", "name2-baz"),
|
||||
)
|
||||
listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := []metav1.PartialObjectMetadata{
|
||||
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"),
|
||||
*newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-baz"),
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
|
||||
t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))
|
||||
}
|
||||
}
|
||||
|
||||
type patchTestCase struct {
|
||||
name string
|
||||
object runtime.Object
|
||||
patchType types.PatchType
|
||||
patchBytes []byte
|
||||
wantErrMsg string
|
||||
expectedPatchedObject runtime.Object
|
||||
}
|
||||
|
||||
func (tc *patchTestCase) runner(t *testing.T) {
|
||||
client := NewSimpleMetadataClient(scheme, tc.object)
|
||||
resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace)
|
||||
|
||||
got, recErr := resourceInterface.Patch(testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{})
|
||||
|
||||
if err := tc.verifyErr(recErr); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := tc.verifyResult(got); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// verifyErr verifies that the given error returned from Patch is the error
|
||||
// expected by the test case.
|
||||
func (tc *patchTestCase) verifyErr(err error) error {
|
||||
if tc.wantErrMsg != "" && err == nil {
|
||||
return fmt.Errorf("want error, got nil")
|
||||
}
|
||||
|
||||
if tc.wantErrMsg == "" && err != nil {
|
||||
return fmt.Errorf("want no error, got %v", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if want, got := tc.wantErrMsg, err.Error(); want != got {
|
||||
return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *patchTestCase) verifyResult(result *metav1.PartialObjectMetadata) error {
|
||||
if tc.expectedPatchedObject == nil && result == nil {
|
||||
return nil
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) {
|
||||
return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
testCases := []patchTestCase{
|
||||
{
|
||||
name: "jsonpatch fails with merge type",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.StrategicMergePatchType,
|
||||
patchBytes: []byte(`[]`),
|
||||
wantErrMsg: "invalid JSON document",
|
||||
}, {
|
||||
name: "jsonpatch works with empty patch",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.JSONPatchType,
|
||||
// No-op
|
||||
patchBytes: []byte(`[]`),
|
||||
expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
}, {
|
||||
name: "jsonpatch works with simple change patch",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.JSONPatchType,
|
||||
// change spec.foo from bar to foobar
|
||||
patchBytes: []byte(`[{"op": "replace", "path": "/metadata/annotations/foo", "value": "foobar"}]`),
|
||||
expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "foobar"}),
|
||||
}, {
|
||||
name: "jsonpatch works with simple addition",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.JSONPatchType,
|
||||
// add spec.newvalue = dummy
|
||||
patchBytes: []byte(`[{"op": "add", "path": "/metadata/annotations/newvalue", "value": "dummy"}]`),
|
||||
expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar", "newvalue": "dummy"}),
|
||||
}, {
|
||||
name: "jsonpatch works with simple deletion",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar", "toremove": "shouldnotbehere"}),
|
||||
patchType: types.JSONPatchType,
|
||||
// remove spec.newvalue = dummy
|
||||
patchBytes: []byte(`[{"op": "remove", "path": "/metadata/annotations/toremove"}]`),
|
||||
expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
}, {
|
||||
name: "strategic merge patch fails with JSONPatch",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.StrategicMergePatchType,
|
||||
// add spec.newvalue = dummy
|
||||
patchBytes: []byte(`[{"op": "add", "path": "/metadata/annotations/newvalue", "value": "dummy"}]`),
|
||||
wantErrMsg: "invalid JSON document",
|
||||
}, {
|
||||
name: "merge patch works with simple replacement",
|
||||
object: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "bar"}),
|
||||
patchType: types.MergePatchType,
|
||||
patchBytes: []byte(`{ "metadata": {"annotations": { "foo": "baz" } } }`),
|
||||
expectedPatchedObject: newPartialObjectMetadataWithAnnotations(map[string]string{"foo": "baz"}),
|
||||
},
|
||||
// TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases
|
||||
// demonstrate expected use cases.
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, tc.runner)
|
||||
}
|
||||
}
|
156
metadata/metadatainformer/informer.go
Normal file
156
metadata/metadatainformer/informer.go
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatainformer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/metadata"
|
||||
"k8s.io/client-go/metadata/metadatalister"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// NewSharedInformerFactory constructs a new instance of metadataSharedInformerFactory for all namespaces.
|
||||
func NewSharedInformerFactory(client metadata.Interface, defaultResync time.Duration) SharedInformerFactory {
|
||||
return NewFilteredSharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil)
|
||||
}
|
||||
|
||||
// NewFilteredSharedInformerFactory constructs a new instance of metadataSharedInformerFactory.
|
||||
// Listers obtained via this factory will be subject to the same filters as specified here.
|
||||
func NewFilteredSharedInformerFactory(client metadata.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) SharedInformerFactory {
|
||||
return &metadataSharedInformerFactory{
|
||||
client: client,
|
||||
defaultResync: defaultResync,
|
||||
namespace: namespace,
|
||||
informers: map[schema.GroupVersionResource]informers.GenericInformer{},
|
||||
startedInformers: make(map[schema.GroupVersionResource]bool),
|
||||
tweakListOptions: tweakListOptions,
|
||||
}
|
||||
}
|
||||
|
||||
type metadataSharedInformerFactory struct {
|
||||
client metadata.Interface
|
||||
defaultResync time.Duration
|
||||
namespace string
|
||||
|
||||
lock sync.Mutex
|
||||
informers map[schema.GroupVersionResource]informers.GenericInformer
|
||||
// startedInformers is used for tracking which informers have been started.
|
||||
// This allows Start() to be called multiple times safely.
|
||||
startedInformers map[schema.GroupVersionResource]bool
|
||||
tweakListOptions TweakListOptionsFunc
|
||||
}
|
||||
|
||||
var _ SharedInformerFactory = &metadataSharedInformerFactory{}
|
||||
|
||||
func (f *metadataSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
key := gvr
|
||||
informer, exists := f.informers[key]
|
||||
if exists {
|
||||
return informer
|
||||
}
|
||||
|
||||
informer = NewFilteredMetadataInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
|
||||
f.informers[key] = informer
|
||||
|
||||
return informer
|
||||
}
|
||||
|
||||
// Start initializes all requested informers.
|
||||
func (f *metadataSharedInformerFactory) Start(stopCh <-chan struct{}) {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
for informerType, informer := range f.informers {
|
||||
if !f.startedInformers[informerType] {
|
||||
go informer.Informer().Run(stopCh)
|
||||
f.startedInformers[informerType] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForCacheSync waits for all started informers' cache were synced.
|
||||
func (f *metadataSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool {
|
||||
informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer {
|
||||
f.lock.Lock()
|
||||
defer f.lock.Unlock()
|
||||
|
||||
informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{}
|
||||
for informerType, informer := range f.informers {
|
||||
if f.startedInformers[informerType] {
|
||||
informers[informerType] = informer.Informer()
|
||||
}
|
||||
}
|
||||
return informers
|
||||
}()
|
||||
|
||||
res := map[schema.GroupVersionResource]bool{}
|
||||
for informType, informer := range informers {
|
||||
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewFilteredMetadataInformer constructs a new informer for a metadata type.
|
||||
func NewFilteredMetadataInformer(client metadata.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer {
|
||||
return &metadataInformer{
|
||||
gvr: gvr,
|
||||
informer: cache.NewSharedIndexInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
if tweakListOptions != nil {
|
||||
tweakListOptions(&options)
|
||||
}
|
||||
return client.Resource(gvr).Namespace(namespace).List(options)
|
||||
},
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
if tweakListOptions != nil {
|
||||
tweakListOptions(&options)
|
||||
}
|
||||
return client.Resource(gvr).Namespace(namespace).Watch(options)
|
||||
},
|
||||
},
|
||||
&metav1.PartialObjectMetadata{},
|
||||
resyncPeriod,
|
||||
indexers,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type metadataInformer struct {
|
||||
informer cache.SharedIndexInformer
|
||||
gvr schema.GroupVersionResource
|
||||
}
|
||||
|
||||
var _ informers.GenericInformer = &metadataInformer{}
|
||||
|
||||
func (d *metadataInformer) Informer() cache.SharedIndexInformer {
|
||||
return d.informer
|
||||
}
|
||||
|
||||
func (d *metadataInformer) Lister() cache.GenericLister {
|
||||
return metadatalister.NewRuntimeObjectShim(metadatalister.New(d.informer.GetIndexer(), d.gvr))
|
||||
}
|
170
metadata/metadatainformer/informer_test.go
Normal file
170
metadata/metadatainformer/informer_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatainformer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/client-go/metadata/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func init() {
|
||||
klog.InitFlags(flag.CommandLine)
|
||||
flag.CommandLine.Lookup("v").Value.Set("5")
|
||||
flag.CommandLine.Lookup("alsologtostderr").Value.Set("true")
|
||||
}
|
||||
|
||||
func TestMetadataSharedInformerFactory(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
existingObj *metav1.PartialObjectMetadata
|
||||
gvr schema.GroupVersionResource
|
||||
ns string
|
||||
trigger func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata
|
||||
handler func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs
|
||||
}{
|
||||
// scenario 1
|
||||
{
|
||||
name: "scenario 1: test if adding an object triggers AddFunc",
|
||||
ns: "ns-foo",
|
||||
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
|
||||
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, _ *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata {
|
||||
testObject := newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo")
|
||||
createdObj, err := fakeClient.Resource(gvr).Namespace(ns).(fake.MetadataClient).CreateFake(testObject, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return createdObj
|
||||
},
|
||||
handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs {
|
||||
return &cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
rcvCh <- obj.(*metav1.PartialObjectMetadata)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// scenario 2
|
||||
{
|
||||
name: "scenario 2: tests if updating an object triggers UpdateFunc",
|
||||
ns: "ns-foo",
|
||||
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
|
||||
existingObj: newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
|
||||
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata {
|
||||
if testObject.Annotations == nil {
|
||||
testObject.Annotations = make(map[string]string)
|
||||
}
|
||||
testObject.Annotations["test"] = "updatedName"
|
||||
updatedObj, err := fakeClient.Resource(gvr).Namespace(ns).(fake.MetadataClient).UpdateFake(testObject, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return updatedObj
|
||||
},
|
||||
handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs {
|
||||
return &cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(old, updated interface{}) {
|
||||
rcvCh <- updated.(*metav1.PartialObjectMetadata)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// scenario 3
|
||||
{
|
||||
name: "scenario 3: test if deleting an object triggers DeleteFunc",
|
||||
ns: "ns-foo",
|
||||
gvr: schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
|
||||
existingObj: newPartialObjectMetadata("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
|
||||
trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeMetadataClient, testObject *metav1.PartialObjectMetadata) *metav1.PartialObjectMetadata {
|
||||
err := fakeClient.Resource(gvr).Namespace(ns).Delete(testObject.GetName(), &metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
return testObject
|
||||
},
|
||||
handler: func(rcvCh chan<- *metav1.PartialObjectMetadata) *cache.ResourceEventHandlerFuncs {
|
||||
return &cache.ResourceEventHandlerFuncs{
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
rcvCh <- obj.(*metav1.PartialObjectMetadata)
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, ts := range scenarios {
|
||||
t.Run(ts.name, func(t *testing.T) {
|
||||
// test data
|
||||
timeout := time.Duration(3 * time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
scheme := runtime.NewScheme()
|
||||
metav1.AddMetaToScheme(scheme)
|
||||
informerReciveObjectCh := make(chan *metav1.PartialObjectMetadata, 1)
|
||||
objs := []runtime.Object{}
|
||||
if ts.existingObj != nil {
|
||||
objs = append(objs, ts.existingObj)
|
||||
}
|
||||
fakeClient := fake.NewSimpleMetadataClient(scheme, objs...)
|
||||
target := NewSharedInformerFactory(fakeClient, 0)
|
||||
|
||||
// act
|
||||
informerListerForGvr := target.ForResource(ts.gvr)
|
||||
informerListerForGvr.Informer().AddEventHandler(ts.handler(informerReciveObjectCh))
|
||||
target.Start(ctx.Done())
|
||||
if synced := target.WaitForCacheSync(ctx.Done()); !synced[ts.gvr] {
|
||||
t.Fatalf("informer for %s hasn't synced", ts.gvr)
|
||||
}
|
||||
|
||||
testObject := ts.trigger(ts.gvr, ts.ns, fakeClient, ts.existingObj)
|
||||
select {
|
||||
case objFromInformer := <-informerReciveObjectCh:
|
||||
if !equality.Semantic.DeepEqual(testObject, objFromInformer) {
|
||||
t.Fatalf("%v", diff.ObjectDiff(testObject, objFromInformer))
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Errorf("tested informer haven't received an object, waited %v", timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata {
|
||||
return &metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: apiVersion,
|
||||
Kind: kind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
34
metadata/metadatainformer/interface.go
Normal file
34
metadata/metadatainformer/interface.go
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatainformer
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/informers"
|
||||
)
|
||||
|
||||
// SharedInformerFactory provides access to a shared informer and lister for dynamic client
|
||||
type SharedInformerFactory interface {
|
||||
Start(stopCh <-chan struct{})
|
||||
ForResource(gvr schema.GroupVersionResource) informers.GenericInformer
|
||||
WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool
|
||||
}
|
||||
|
||||
// TweakListOptionsFunc defines the signature of a helper function
|
||||
// that wants to provide more listing options to API
|
||||
type TweakListOptionsFunc func(*metav1.ListOptions)
|
40
metadata/metadatalister/interface.go
Normal file
40
metadata/metadatalister/interface.go
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatalister
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
)
|
||||
|
||||
// Lister helps list resources.
|
||||
type Lister interface {
|
||||
// List lists all resources in the indexer.
|
||||
List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error)
|
||||
// Get retrieves a resource from the indexer with the given name
|
||||
Get(name string) (*metav1.PartialObjectMetadata, error)
|
||||
// Namespace returns an object that can list and get resources in a given namespace.
|
||||
Namespace(namespace string) NamespaceLister
|
||||
}
|
||||
|
||||
// NamespaceLister helps list and get resources.
|
||||
type NamespaceLister interface {
|
||||
// List lists all resources in the indexer for a given namespace.
|
||||
List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error)
|
||||
// Get retrieves a resource from the indexer for a given namespace and name.
|
||||
Get(name string) (*metav1.PartialObjectMetadata, error)
|
||||
}
|
91
metadata/metadatalister/lister.go
Normal file
91
metadata/metadatalister/lister.go
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatalister
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
var _ Lister = &metadataLister{}
|
||||
var _ NamespaceLister = &metadataNamespaceLister{}
|
||||
|
||||
// metadataLister implements the Lister interface.
|
||||
type metadataLister struct {
|
||||
indexer cache.Indexer
|
||||
gvr schema.GroupVersionResource
|
||||
}
|
||||
|
||||
// New returns a new Lister.
|
||||
func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister {
|
||||
return &metadataLister{indexer: indexer, gvr: gvr}
|
||||
}
|
||||
|
||||
// List lists all resources in the indexer.
|
||||
func (l *metadataLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) {
|
||||
err = cache.ListAll(l.indexer, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*metav1.PartialObjectMetadata))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get retrieves a resource from the indexer with the given name
|
||||
func (l *metadataLister) Get(name string) (*metav1.PartialObjectMetadata, error) {
|
||||
obj, exists, err := l.indexer.GetByKey(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.NewNotFound(l.gvr.GroupResource(), name)
|
||||
}
|
||||
return obj.(*metav1.PartialObjectMetadata), nil
|
||||
}
|
||||
|
||||
// Namespace returns an object that can list and get resources from a given namespace.
|
||||
func (l *metadataLister) Namespace(namespace string) NamespaceLister {
|
||||
return &metadataNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr}
|
||||
}
|
||||
|
||||
// metadataNamespaceLister implements the NamespaceLister interface.
|
||||
type metadataNamespaceLister struct {
|
||||
indexer cache.Indexer
|
||||
namespace string
|
||||
gvr schema.GroupVersionResource
|
||||
}
|
||||
|
||||
// List lists all resources in the indexer for a given namespace.
|
||||
func (l *metadataNamespaceLister) List(selector labels.Selector) (ret []*metav1.PartialObjectMetadata, err error) {
|
||||
err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) {
|
||||
ret = append(ret, m.(*metav1.PartialObjectMetadata))
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get retrieves a resource from the indexer for a given namespace and name.
|
||||
func (l *metadataNamespaceLister) Get(name string) (*metav1.PartialObjectMetadata, error) {
|
||||
obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.NewNotFound(l.gvr.GroupResource(), name)
|
||||
}
|
||||
return obj.(*metav1.PartialObjectMetadata), nil
|
||||
}
|
256
metadata/metadatalister/lister_test.go
Normal file
256
metadata/metadatalister/lister_test.go
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatalister
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func TestNamespaceGetMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingObjects []runtime.Object
|
||||
namespaceToSync string
|
||||
gvrToSync schema.GroupVersionResource
|
||||
objectToGet string
|
||||
expectedObject *metav1.PartialObjectMetadata
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "scenario 1: gets name-foo1 resource from the indexer from ns-foo namespace",
|
||||
existingObjects: []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"),
|
||||
},
|
||||
namespaceToSync: "ns-foo",
|
||||
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
|
||||
objectToGet: "name-foo1",
|
||||
expectedObject: newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
},
|
||||
{
|
||||
name: "scenario 2: gets name-foo-non-existing resource from the indexer from ns-foo namespace",
|
||||
existingObjects: []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"),
|
||||
},
|
||||
namespaceToSync: "ns-foo",
|
||||
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
|
||||
objectToGet: "name-foo-non-existing",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// test data
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
|
||||
for _, obj := range test.existingObjects {
|
||||
err := indexer.Add(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// act
|
||||
target := New(indexer, test.gvrToSync).Namespace(test.namespaceToSync)
|
||||
actualObject, err := target.Get(test.objectToGet)
|
||||
|
||||
// validate
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("expected to get an error but non was returned")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedObject, actualObject) {
|
||||
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceListMethod(t *testing.T) {
|
||||
// test data
|
||||
objs := []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"),
|
||||
}
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
|
||||
for _, obj := range objs {
|
||||
err := indexer.Add(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
expectedOutput := []*metav1.PartialObjectMetadata{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
}
|
||||
namespaceToList := "ns-foo"
|
||||
|
||||
// act
|
||||
target := New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"}).Namespace(namespaceToList)
|
||||
actualOutput, err := target.List(labels.Everything())
|
||||
|
||||
// validate
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertListOrDie(expectedOutput, actualOutput, t)
|
||||
}
|
||||
|
||||
func TestListerGetMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
existingObjects []runtime.Object
|
||||
namespaceToSync string
|
||||
gvrToSync schema.GroupVersionResource
|
||||
objectToGet string
|
||||
expectedObject *metav1.PartialObjectMetadata
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "scenario 1: gets name-foo1 resource from the indexer",
|
||||
existingObjects: []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "", "name-foo1"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"),
|
||||
},
|
||||
namespaceToSync: "",
|
||||
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
|
||||
objectToGet: "name-foo1",
|
||||
expectedObject: newPartialObjectMetadata("group/version", "TheKind", "", "name-foo1"),
|
||||
},
|
||||
{
|
||||
name: "scenario 2: doesn't get name-foo resource from the indexer from ns-foo namespace",
|
||||
existingObjects: []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo1"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-bar", "name-bar"),
|
||||
},
|
||||
namespaceToSync: "ns-foo",
|
||||
gvrToSync: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"},
|
||||
objectToGet: "name-foo",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// test data
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
|
||||
for _, obj := range test.existingObjects {
|
||||
err := indexer.Add(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// act
|
||||
target := New(indexer, test.gvrToSync)
|
||||
actualObject, err := target.Get(test.objectToGet)
|
||||
|
||||
// validate
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Fatal("expected to get an error but non was returned")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedObject, actualObject) {
|
||||
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", test.expectedObject, actualObject, diff.ObjectDiff(test.expectedObject, actualObject))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListerListMethod(t *testing.T) {
|
||||
// test data
|
||||
objs := []runtime.Object{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"),
|
||||
}
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
|
||||
for _, obj := range objs {
|
||||
err := indexer.Add(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
expectedOutput := []*metav1.PartialObjectMetadata{
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-foo"),
|
||||
newPartialObjectMetadata("group/version", "TheKind", "ns-foo", "name-bar"),
|
||||
}
|
||||
|
||||
// act
|
||||
target := New(indexer, schema.GroupVersionResource{Group: "group", Version: "version", Resource: "TheKinds"})
|
||||
actualOutput, err := target.List(labels.Everything())
|
||||
|
||||
// validate
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertListOrDie(expectedOutput, actualOutput, t)
|
||||
}
|
||||
|
||||
func assertListOrDie(expected, actual []*metav1.PartialObjectMetadata, t *testing.T) {
|
||||
if len(actual) != len(expected) {
|
||||
t.Fatalf("unexpected number of items returned, expected = %d, actual = %d", len(expected), len(actual))
|
||||
}
|
||||
for _, expectedObject := range expected {
|
||||
found := false
|
||||
for _, actualObject := range actual {
|
||||
if actualObject.GetName() == expectedObject.GetName() {
|
||||
if !reflect.DeepEqual(expectedObject, actualObject) {
|
||||
t.Fatalf("unexpected object has been returned expected = %v actual = %v, diff = %v", expectedObject, actualObject, diff.ObjectDiff(expectedObject, actualObject))
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the resource with the name = %s was not found in the returned output", expectedObject.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newPartialObjectMetadata(apiVersion, kind, namespace, name string) *metav1.PartialObjectMetadata {
|
||||
return &metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: apiVersion,
|
||||
Kind: kind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
}
|
87
metadata/metadatalister/shim.go
Normal file
87
metadata/metadatalister/shim.go
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
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 metadatalister
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
var _ cache.GenericLister = &metadataListerShim{}
|
||||
var _ cache.GenericNamespaceLister = &metadataNamespaceListerShim{}
|
||||
|
||||
// metadataListerShim implements the cache.GenericLister interface.
|
||||
type metadataListerShim struct {
|
||||
lister Lister
|
||||
}
|
||||
|
||||
// NewRuntimeObjectShim returns a new shim for Lister.
|
||||
// It wraps Lister so that it implements cache.GenericLister interface
|
||||
func NewRuntimeObjectShim(lister Lister) cache.GenericLister {
|
||||
return &metadataListerShim{lister: lister}
|
||||
}
|
||||
|
||||
// List will return all objects across namespaces
|
||||
func (s *metadataListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) {
|
||||
objs, err := s.lister.List(selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = make([]runtime.Object, len(objs))
|
||||
for index, obj := range objs {
|
||||
ret[index] = obj
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get will attempt to retrieve assuming that name==key
|
||||
func (s *metadataListerShim) Get(name string) (runtime.Object, error) {
|
||||
return s.lister.Get(name)
|
||||
}
|
||||
|
||||
func (s *metadataListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister {
|
||||
return &metadataNamespaceListerShim{
|
||||
namespaceLister: s.lister.Namespace(namespace),
|
||||
}
|
||||
}
|
||||
|
||||
// metadataNamespaceListerShim implements the NamespaceLister interface.
|
||||
// It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface
|
||||
type metadataNamespaceListerShim struct {
|
||||
namespaceLister NamespaceLister
|
||||
}
|
||||
|
||||
// List will return all objects in this namespace
|
||||
func (ns *metadataNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) {
|
||||
objs, err := ns.namespaceLister.List(selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = make([]runtime.Object, len(objs))
|
||||
for index, obj := range objs {
|
||||
ret[index] = obj
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Get will attempt to retrieve by namespace and name
|
||||
func (ns *metadataNamespaceListerShim) Get(name string) (runtime.Object, error) {
|
||||
return ns.namespaceLister.Get(name)
|
||||
}
|
Loading…
Reference in New Issue
Block a user