Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions clientutils/fieldindexer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2022 OnMetal 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 clientutils

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
)

// SharedFieldIndexer allows registering and calling field index functions shared by different users.
type SharedFieldIndexer struct {
indexer client.FieldIndexer
*sharedFieldIndexerMap
}

// NewSharedFieldIndexer creates a new SharedFieldIndexer.
func NewSharedFieldIndexer(indexer client.FieldIndexer, scheme *runtime.Scheme) *SharedFieldIndexer {
return &SharedFieldIndexer{
indexer: indexer,
sharedFieldIndexerMap: newSharedFieldIndexerMap(scheme),
}
}

// Register registers the client.IndexerFunc for the given client.Object and field.
func (s *SharedFieldIndexer) Register(obj client.Object, field string, extractValue client.IndexerFunc) error {
updated, err := s.setIfNotPresent(obj, field, extractValue)
if err != nil {
return err
}
if !updated {
return fmt.Errorf("indexer for type %T field %s already registered", obj, field)
}
return nil
}

// MustRegister registers the client.IndexerFunc for the given client.Object and field.
func (s *SharedFieldIndexer) MustRegister(obj client.Object, field string, extractValue client.IndexerFunc) {
utilruntime.Must(s.Register(obj, field, extractValue))
}

// IndexField calls a registered client.IndexerFunc for the given client.Object and field.
// If the object / field is unknown or its GVK could not be determined, it errors.
func (s *SharedFieldIndexer) IndexField(ctx context.Context, obj client.Object, field string) error {
entry, err := s.get(obj, field)
if err != nil {
return err
}

if entry == nil {
return fmt.Errorf("unknown field %s for type %T", field, obj)
}
if entry.initialized {
return nil
}
if err := s.indexer.IndexField(ctx, obj, field, entry.extractValue); err != nil {
return err
}
entry.initialized = true
return nil
}

type sharedFieldIndexerMap struct {
scheme *runtime.Scheme
unstructured *specificSharedFieldIndexerMap
metadata *specificSharedFieldIndexerMap
structured *specificSharedFieldIndexerMap
}

func newSharedFieldIndexerMap(scheme *runtime.Scheme) *sharedFieldIndexerMap {
return &sharedFieldIndexerMap{
scheme: scheme,
unstructured: newSpecificSharedFieldIndexerMap(),
metadata: newSpecificSharedFieldIndexerMap(),
structured: newSpecificSharedFieldIndexerMap(),
}
}

type mapEntry struct {
initialized bool
extractValue client.IndexerFunc
}

type specificSharedFieldIndexerMap struct {
gvkToNameToEntry map[schema.GroupVersionKind]map[string]*mapEntry
}

func newSpecificSharedFieldIndexerMap() *specificSharedFieldIndexerMap {
return &specificSharedFieldIndexerMap{gvkToNameToEntry: make(map[schema.GroupVersionKind]map[string]*mapEntry)}
}

func (s *specificSharedFieldIndexerMap) get(gvk schema.GroupVersionKind, name string) *mapEntry {
return s.gvkToNameToEntry[gvk][name]
}

func (s *specificSharedFieldIndexerMap) setIfNotPresent(gvk schema.GroupVersionKind, name string, extractValue client.IndexerFunc) (updated bool) {
nameToEntry := s.gvkToNameToEntry[gvk]
if nameToEntry == nil {
nameToEntry = make(map[string]*mapEntry)
s.gvkToNameToEntry[gvk] = nameToEntry
}

if _, ok := nameToEntry[name]; ok {
return false
}
nameToEntry[name] = &mapEntry{extractValue: extractValue}
return true
}

func (s *sharedFieldIndexerMap) mapFor(obj client.Object) (*specificSharedFieldIndexerMap, schema.GroupVersionKind, error) {
gvk, err := apiutil.GVKForObject(obj, s.scheme)
if err != nil {
return nil, schema.GroupVersionKind{}, err
}

switch obj.(type) {
case *unstructured.Unstructured:
return s.unstructured, gvk, nil
case *metav1.PartialObjectMetadata:
return s.metadata, gvk, nil
default:
return s.structured, gvk, nil
}
}

func (s *sharedFieldIndexerMap) get(obj client.Object, name string) (*mapEntry, error) {
m, gvk, err := s.mapFor(obj)
if err != nil {
return nil, err
}

return m.get(gvk, name), nil
}

func (s *sharedFieldIndexerMap) setIfNotPresent(obj client.Object, name string, extractValue client.IndexerFunc) (updated bool, err error) {
m, gvk, err := s.mapFor(obj)
if err != nil {
return false, err
}

return m.setIfNotPresent(gvk, name, extractValue), nil
}
160 changes: 160 additions & 0 deletions clientutils/fieldindexer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2021 OnMetal 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 clientutils

import (
"context"

"github.com/golang/mock/gomock"
mockclient "github.com/onmetal/controller-utils/mock/controller-runtime/client"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ = Describe("FieldIndexer", func() {
var (
ctx context.Context
ctrl *gomock.Controller
)
BeforeEach(func() {
ctx = context.Background()
ctrl = gomock.NewController(GinkgoT())
})
AfterEach(func() {
ctrl.Finish()
})

Context("SharedFieldIndexer", func() {
var (
fieldIndexer *mockclient.MockFieldIndexer
)
BeforeEach(func() {
fieldIndexer = mockclient.NewMockFieldIndexer(ctrl)
})

It("should register an indexer func and call it", func() {
f := mockclient.NewMockIndexerFunc(ctrl)
gomock.InOrder(
fieldIndexer.EXPECT().IndexField(ctx, &corev1.Pod{}, ".spec", gomock.Any()).Do(
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
f(obj)
return nil
}),
f.EXPECT().Call(&corev1.Pod{}).Times(1),
)

idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())

Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
})

It("should error if a field is indexed twice", func() {
f := mockclient.NewMockIndexerFunc(ctrl)
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())
Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(MatchError("indexer for type *v1.Pod field .spec already registered"))
})

It("should call the index function only once", func() {
f := mockclient.NewMockIndexerFunc(ctrl)
gomock.InOrder(
fieldIndexer.EXPECT().IndexField(ctx, &corev1.Pod{}, ".spec", gomock.Any()).Do(
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
f(obj)
return nil
}),
f.EXPECT().Call(&corev1.Pod{}).Times(1),
)

idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())

Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
})

It("should work with unstructured objects", func() {
pod := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
},
}
f := mockclient.NewMockIndexerFunc(ctrl)
gomock.InOrder(
fieldIndexer.EXPECT().IndexField(ctx, pod, ".spec", gomock.Any()).Do(
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
f(obj)
return nil
}),
f.EXPECT().Call(pod).Times(1),
)

idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

Expect(idx.Register(pod, ".spec", f.Call)).To(Succeed())

Expect(idx.IndexField(ctx, pod, ".spec")).To(Succeed())
})

It("should work with partial object metadata", func() {
pod := &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
}
f := mockclient.NewMockIndexerFunc(ctrl)
gomock.InOrder(
fieldIndexer.EXPECT().IndexField(ctx, pod, ".spec", gomock.Any()).Do(
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
f(obj)
return nil
}),
f.EXPECT().Call(pod).Times(1),
)

idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

Expect(idx.Register(pod, ".spec", f.Call)).To(Succeed())

Expect(idx.IndexField(ctx, pod, ".spec")).To(Succeed())
})

It("should error if the gvk could not be obtained", func() {
f := mockclient.NewMockIndexerFunc(ctrl)
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)

type CustomObject struct{ corev1.Pod }

Expect(idx.Register(&CustomObject{}, ".spec", f.Call)).To(HaveOccurred())
})

It("should error if the indexed function is unknown", func() {
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
Expect(idx.IndexField(ctx, &corev1.Pod{}, "unknown")).To(HaveOccurred())
})
})
})
9 changes: 8 additions & 1 deletion mock/controller-runtime/client/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@
// limitations under the License.

// Package client contains mocks for controller-runtime's client package.
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination mocks.go sigs.k8s.io/controller-runtime/pkg/client Client
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination mocks.go sigs.k8s.io/controller-runtime/pkg/client Client,FieldIndexer
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination funcs.go github.com/onmetal/controller-utils/mock/controller-runtime/client IndexerFunc
package client

import "sigs.k8s.io/controller-runtime/pkg/client"

type IndexerFunc interface {
Call(object client.Object) []string
}
Loading