Skip to content

Commit

Permalink
Add ReflectiveResourceList
Browse files Browse the repository at this point in the history
This type implements ResourceList using reflection, so that consumers
don't need to implement the methods themselves. FakePolicyController
tests verify that the behavior of using this implementation matches the
behavior of the bespoke implementation.

Signed-off-by: Justin Kulikauskas <[email protected]>
  • Loading branch information
JustinKuli committed May 4, 2024
1 parent 03e5dc9 commit a65fcc0
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 1 deletion.
168 changes: 168 additions & 0 deletions api/v1beta1/clientobjectfakes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package v1beta1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// fakeObjList implements client.ObjectList
type fakeObjList string

var _ client.ObjectList = fakeObjList("")

func (l fakeObjList) GetResourceVersion() string {
return string(l)
}

func (l fakeObjList) SetResourceVersion(version string) {
}

func (l fakeObjList) GetSelfLink() string {
return string(l)
}

func (l fakeObjList) SetSelfLink(selfLink string) {
}

func (l fakeObjList) GetContinue() string {
return string(l)
}

func (l fakeObjList) SetContinue(c string) {
}

func (l fakeObjList) GetRemainingItemCount() *int64 {
return nil
}

func (l fakeObjList) SetRemainingItemCount(c *int64) {
}

func (l fakeObjList) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}

func (l fakeObjList) DeepCopyObject() runtime.Object {
return l
}

// fakeObjList implements client.Object
type fakeObj string

var _ client.Object = fakeObj("")

func (o fakeObj) GetNamespace() string {
return string(o)
}

func (o fakeObj) SetNamespace(namespace string) {
}

func (o fakeObj) GetName() string {
return string(o)
}

func (o fakeObj) SetName(name string) {
}

func (o fakeObj) GetGenerateName() string {
return string(o)
}

func (o fakeObj) SetGenerateName(name string) {
}

func (o fakeObj) GetUID() types.UID {
return types.UID(o)
}

func (o fakeObj) SetUID(uid types.UID) {
}

func (o fakeObj) GetResourceVersion() string {
return string(o)
}

func (o fakeObj) SetResourceVersion(version string) {
}

func (o fakeObj) GetGeneration() int64 {
return 0
}

func (o fakeObj) SetGeneration(generation int64) {
}

func (o fakeObj) GetSelfLink() string {
return string(o)
}

func (o fakeObj) SetSelfLink(selfLink string) {
}

func (o fakeObj) GetCreationTimestamp() metav1.Time {
return metav1.Now()
}

func (o fakeObj) SetCreationTimestamp(timestamp metav1.Time) {
}

func (o fakeObj) GetDeletionTimestamp() *metav1.Time {
return nil
}

func (o fakeObj) SetDeletionTimestamp(timestamp *metav1.Time) {
}

func (o fakeObj) GetDeletionGracePeriodSeconds() *int64 {
return nil
}

func (o fakeObj) SetDeletionGracePeriodSeconds(*int64) {
}

func (o fakeObj) GetLabels() map[string]string {
return nil
}

func (o fakeObj) SetLabels(labels map[string]string) {
}

func (o fakeObj) GetAnnotations() map[string]string {
return nil
}

func (o fakeObj) SetAnnotations(annotations map[string]string) {
}

func (o fakeObj) GetFinalizers() []string {
return nil
}

func (o fakeObj) SetFinalizers(finalizers []string) {
}

func (o fakeObj) GetOwnerReferences() []metav1.OwnerReference {
return nil
}

func (o fakeObj) SetOwnerReferences([]metav1.OwnerReference) {
}

func (o fakeObj) GetManagedFields() []metav1.ManagedFieldsEntry {
return nil
}

func (o fakeObj) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {
}

func (o fakeObj) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}

func (o fakeObj) DeepCopyObject() runtime.Object {
return o
}
85 changes: 85 additions & 0 deletions api/v1beta1/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"fmt"
"path/filepath"
"reflect"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -36,6 +37,90 @@ type ResourceList interface {
Items() ([]client.Object, error)
}

//+kubebuilder:object:generate=false

// ReflectiveResourceList implements ResourceList for the wrapped client.ObjectList, by using
// reflection. The wrapped list must have an Items field, with a slice of items which satisfy the
// client.Object interface - most types which satisfy client.ObjectList seem to follow this
// convention. Using this type is not recommended: implementing ResourceList yourself will generally
// lead to better performance.
type ReflectiveResourceList struct {
ClientList client.ObjectList
}

// Items returns the list of items in the list. Since this implementation uses reflection, it may
// have errors or not perform as well as a bespoke implementation for the underlying type.
func (l *ReflectiveResourceList) Items() ([]client.Object, error) {
value := reflect.ValueOf(l.ClientList)
if value.Kind() == reflect.Pointer {
value = value.Elem()
}

if value.Kind() != reflect.Struct {
return nil, &ReflectiveResourceListError{
typeName: value.Type().PkgPath() + "." + value.Type().Name(),
message: "the underlying go Kind was not a struct",
}
}

itemsField := value.FieldByName("Items")
if !itemsField.IsValid() {
return nil, &ReflectiveResourceListError{
typeName: value.Type().PkgPath() + "." + value.Type().Name(),
message: "the underlying struct does not have a field called 'Items'",
}
}

if itemsField.Kind() != reflect.Slice {
return nil, &ReflectiveResourceListError{
typeName: value.Type().PkgPath() + "." + value.Type().Name(),
message: "the 'Items' field in the underlying struct isn't a slice",
}
}

items := make([]client.Object, itemsField.Len())

for i := 0; i < itemsField.Len(); i++ {
item, ok := itemsField.Index(i).Interface().(client.Object)
if ok {
items[i] = item

continue
}

// Try a pointer receiver
item, ok = itemsField.Index(i).Addr().Interface().(client.Object)
if ok {
items[i] = item

continue
}

return nil, &ReflectiveResourceListError{
typeName: value.Type().PkgPath() + "." + value.Type().Name(),
message: "an item in the underlying struct's 'Items' slice could not be " +
"type-asserted to a sigs.k8s.io/controller-runtime/pkg/client.Object",
}
}

return items, nil
}

func (l *ReflectiveResourceList) ObjectList() client.ObjectList {
return l.ClientList
}

//+kubebuilder:object:generate=false

type ReflectiveResourceListError struct {
typeName string
message string
}

func (e *ReflectiveResourceListError) Error() string {
return fmt.Sprintf("unable to use %v as a nucleus ResourceList: %v", e.typeName, e.message)
}

// GetMatches returns a list of resources on the cluster, matched by the Target. The provided
// ResourceList should be backed by a client.ObjectList type which must registered in the scheme of
// the client.Reader. The items in the provided ResourceList after this method is called will not
Expand Down
85 changes: 85 additions & 0 deletions api/v1beta1/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ package v1beta1

import (
"errors"
"fmt"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var sampleNames = []string{
Expand Down Expand Up @@ -251,3 +254,85 @@ func TestMatchesErrors(t *testing.T) {
}
}
}

func TestReflectiveItemsErrors(t *testing.T) {
t.Parallel()

type wrappedFakeObjList struct {
fakeObjList
}

type wrappedWithIntItems struct {
fakeObjList
Items int
}

type wrappedWithBadItems struct {
fakeObjList
Items []int
}

type wrappedWithGoodItems struct {
fakeObjList
Items []fakeObj
}

tests := map[string]struct {
list client.ObjectList
msg string
}{
"NamespaceList": {
list: &corev1.NamespaceList{Items: []corev1.Namespace{{}}},
msg: "",
},
"fakeObjList": {
list: fakeObjList(""),
msg: "the underlying go Kind was not a struct",
},
"wrappedFakeObjList": {
list: wrappedFakeObjList{fakeObjList("")},
msg: "the underlying struct does not have a field called 'Items'",
},
"wrappedWithIntItems": {
list: wrappedWithIntItems{fakeObjList: fakeObjList("")},
msg: "the 'Items' field in the underlying struct isn't a slice",
},
"wrappedWithEmptyItems": {
list: wrappedWithBadItems{fakeObjList: fakeObjList(""), Items: []int{}},
msg: "",
},
"wrappedWithBadItems": {
list: wrappedWithBadItems{fakeObjList: fakeObjList(""), Items: []int{0}},
msg: "an item in the underlying struct's 'Items' slice could not be type-asserted " +
"to a sigs.k8s.io/controller-runtime/pkg/client.Object",
},
"wrappedWithGoodItems": {
list: wrappedWithGoodItems{fakeObjList: fakeObjList(""), Items: []fakeObj{fakeObj("")}},
msg: "",
},
}

for name, tcase := range tests {
refList := ReflectiveResourceList{ClientList: tcase.list}

_, err := refList.Items()

if tcase.msg == "" {
if err != nil {
t.Errorf("Unexpected error in test '%v', expected nil, got %v", name, err)
}
} else {
if err == nil {
t.Errorf("Expected an error in test '%v', but got nil", name)
}

wantErr := fmt.Sprintf("unable to use open-cluster-management.io/governance-policy-nucleus/api/v1beta1."+
"%v as a nucleus ResourceList: %v", name, tcase.msg)

diff := cmp.Diff(wantErr, err.Error())
if diff != "" {
t.Errorf("Error mismatch in test '%v', diff: '%v'", name, diff)
}
}
}
}
3 changes: 3 additions & 0 deletions test/fakepolicy/api/v1beta1/fakepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type FakePolicySpec struct {

// targetConfigMaps defines the ConfigMaps which should be examined by this policy
TargetConfigMaps nucleusv1beta1.Target `json:"targetConfigMaps,omitempty"`

// targetUsingReflection defines whether to use reflection to find the ConfigMaps
TargetUsingReflection bool `json:"targetUsingReflection,omitempty"`
}

//+kubebuilder:validation:Optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ spec:
namespaces.
type: string
type: object
targetUsingReflection:
description: targetUsingReflection defines whether to use reflection
to find the ConfigMaps
type: boolean
type: object
status:
description: FakePolicyStatus defines the observed state of FakePolicy
Expand Down
Loading

0 comments on commit a65fcc0

Please sign in to comment.